pwn 0.5.784 → 0.5.787

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 (254) 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/build_gem.sh +1 -1
  8. data/documentation/AI-Integration.md +7 -5
  9. data/documentation/Agent-Tool-Registry.md +2 -2
  10. data/documentation/Backup-Retention.md +69 -15
  11. data/documentation/Configuration.md +10 -6
  12. data/documentation/Contributing.md +1 -1
  13. data/documentation/Cron.md +2 -2
  14. data/documentation/Diagrams.md +2 -2
  15. data/documentation/General-PWN-Usage.md +4 -3
  16. data/documentation/Harness-Evaluation.md +1 -1
  17. data/documentation/Home.md +6 -5
  18. data/documentation/How-PWN-Works.md +6 -5
  19. data/documentation/Installation.md +12 -9
  20. data/documentation/Mission-Cancellation.md +134 -0
  21. data/documentation/Operability.md +18 -0
  22. data/documentation/Persistence.md +7 -5
  23. data/documentation/Plugin-Degradation.md +14 -0
  24. data/documentation/Plugins.md +3 -2
  25. data/documentation/Policy-Benchmark.md +32 -33
  26. data/documentation/Runtime-Source-Protection.md +162 -0
  27. data/documentation/Transparent-Browser.md +11 -0
  28. data/documentation/Troubleshooting.md +3 -2
  29. data/documentation/What-is-PWN.md +2 -2
  30. data/documentation/YubiKey-Vaults.md +185 -0
  31. data/documentation/YubiKey.md +218 -0
  32. data/documentation/diagrams/cron-scheduling.svg +130 -130
  33. data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
  34. data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
  35. data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
  36. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
  37. data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
  38. data/documentation/diagrams/persistence-filesystem.svg +172 -142
  39. data/documentation/diagrams/plugin-ecosystem.svg +64 -64
  40. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
  41. data/documentation/diagrams/reinforcement-learning.svg +89 -89
  42. data/documentation/pwn-ai-Agent.md +10 -4
  43. data/etc/default_cron/session_cleanup.rb +43 -27
  44. data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
  45. data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
  46. data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
  47. data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
  48. data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
  49. data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
  50. data/etc/default_skills/pwn/backups/SKILL.md +54 -0
  51. data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
  52. data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
  53. data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
  54. data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
  55. data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
  56. data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
  57. data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
  58. data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
  59. data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
  60. data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
  61. data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
  62. data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
  63. data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
  64. data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
  65. data/lib/pwn/ai/agent/dispatch.rb +26 -15
  66. data/lib/pwn/ai/agent/learning.rb +14 -18
  67. data/lib/pwn/ai/agent/loop.rb +17 -1
  68. data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
  69. data/lib/pwn/ai/agent/policy_evaluation.rb +334 -36
  70. data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
  71. data/lib/pwn/ai/agent/request_runtime.rb +122 -18
  72. data/lib/pwn/ai/agent/reward.rb +5 -3
  73. data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
  74. data/lib/pwn/ai/agent/skill_review.rb +9 -5
  75. data/lib/pwn/ai/agent/swarm.rb +4 -0
  76. data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
  77. data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
  78. data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
  79. data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
  80. data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
  81. data/lib/pwn/ai/agent.rb +1 -0
  82. data/lib/pwn/ai/anthropic.rb +8 -33
  83. data/lib/pwn/ai/cli.rb +34 -5
  84. data/lib/pwn/ai/gemini.rb +1 -0
  85. data/lib/pwn/ai/grok.rb +39 -28
  86. data/lib/pwn/ai/ollama.rb +4 -2
  87. data/lib/pwn/ai/open_ai.rb +11 -32
  88. data/lib/pwn/ai/open_web_ui.rb +62 -4
  89. data/lib/pwn/ai/router.rb +8 -1
  90. data/lib/pwn/ai/server_tools.rb +231 -0
  91. data/lib/pwn/ai.rb +1 -0
  92. data/lib/pwn/backups.rb +349 -0
  93. data/lib/pwn/config.rb +70 -39
  94. data/lib/pwn/migrate.rb +85 -280
  95. data/lib/pwn/module_skills.rb +7 -0
  96. data/lib/pwn/plugins/ai_sandbox.rb +538 -1
  97. data/lib/pwn/plugins/authentication_helper.rb +74 -4
  98. data/lib/pwn/plugins/basic_auth.rb +2 -1
  99. data/lib/pwn/plugins/google_workspace.rb +4 -21
  100. data/lib/pwn/plugins/jobs.rb +1 -1
  101. data/lib/pwn/plugins/log.rb +3 -1
  102. data/lib/pwn/plugins/preflight_checker.rb +34 -9
  103. data/lib/pwn/plugins/recon.rb +4 -1
  104. data/lib/pwn/plugins/repl/ai.rb +293 -26
  105. data/lib/pwn/plugins/repl/ai_console.rb +268 -88
  106. data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
  107. data/lib/pwn/plugins/repl/mesh.rb +14 -19
  108. data/lib/pwn/plugins/repl/vault.rb +4 -15
  109. data/lib/pwn/plugins/repl.rb +9 -216
  110. data/lib/pwn/plugins/sandbox/driver.rb +4 -0
  111. data/lib/pwn/plugins/transparent_browser.rb +215 -0
  112. data/lib/pwn/plugins/vault.rb +412 -135
  113. data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
  114. data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
  115. data/lib/pwn/plugins/yubikey/management.rb +686 -0
  116. data/lib/pwn/plugins/yubikey/oath.rb +766 -0
  117. data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
  118. data/lib/pwn/plugins/yubikey/otp.rb +897 -0
  119. data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
  120. data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
  121. data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
  122. data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
  123. data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
  124. data/lib/pwn/plugins/yubikey.rb +844 -0
  125. data/lib/pwn/plugins.rb +1 -0
  126. data/lib/pwn/redaction.rb +69 -12
  127. data/lib/pwn/session_trace.rb +4 -0
  128. data/lib/pwn/setup.rb +114 -24
  129. data/lib/pwn/version.rb +1 -1
  130. data/lib/pwn/www/app_cobalt_io.rb +16 -5
  131. data/lib/pwn/www/bug_crowd.rb +16 -5
  132. data/lib/pwn/www/coinbase_pro.rb +16 -5
  133. data/lib/pwn/www/github.rb +30 -14
  134. data/lib/pwn/www/paypal.rb +15 -4
  135. data/lib/pwn/www/synack.rb +16 -5
  136. data/lib/pwn/www/twitter.rb +17 -6
  137. data/lib/pwn.rb +1 -0
  138. data/spec/documentation/installation_md_spec.rb +1 -1
  139. data/spec/fixtures/yubikey/apdu_frames.json +162 -0
  140. data/spec/integration/authentication_helper_oath_spec.rb +117 -0
  141. data/spec/integration/controller_spill_spec.rb +48 -0
  142. data/spec/integration/harness_process_spec.rb +97 -0
  143. data/spec/integration/mission_cancel_spec.rb +79 -0
  144. data/spec/integration/mission_curses_pty_spec.rb +44 -0
  145. data/spec/integration/readme_security_spec.rb +47 -0
  146. data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
  147. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
  148. data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
  149. data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
  150. data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
  151. data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
  152. data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
  153. data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
  154. data/spec/lib/pwn/ai/agent/policy_evaluation_spec.rb +10 -23
  155. data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
  156. data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
  157. data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
  158. data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
  159. data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
  160. data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
  161. data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
  162. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
  163. data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
  164. data/spec/lib/pwn/ai/cli_spec.rb +8 -16
  165. data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
  166. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
  167. data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
  168. data/spec/lib/pwn/ai/router_spec.rb +1 -1
  169. data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
  170. data/spec/lib/pwn/backups_spec.rb +129 -0
  171. data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
  172. data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
  173. data/spec/lib/pwn/migrate_spec.rb +29 -81
  174. data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
  175. data/spec/lib/pwn/module_skills_spec.rb +8 -0
  176. data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
  177. data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
  178. data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
  179. data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
  180. data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
  181. data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
  182. data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
  183. data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
  184. data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
  185. data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +110 -75
  186. data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
  187. data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -16
  188. data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
  189. data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
  190. data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
  191. data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
  192. data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
  193. data/spec/lib/pwn/plugins/repl_mesh_layout_spec.rb +2 -0
  194. data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
  195. data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
  196. data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
  197. data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
  198. data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
  199. data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
  200. data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
  201. data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
  202. data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
  203. data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
  204. data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
  205. data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
  206. data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
  207. data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
  208. data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
  209. data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
  210. data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
  211. data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
  212. data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
  213. data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
  214. data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
  215. data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
  216. data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
  217. data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
  218. data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
  219. data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
  220. data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
  221. data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
  222. data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
  223. data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
  224. data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
  225. data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
  226. data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
  227. data/spec/spec_helper.rb +15 -0
  228. data/spec/support/harness/cli_faults.rb +13 -9
  229. data/spec/support/harness/controller_spill.rb +26 -0
  230. data/spec/support/harness/controller_state.rb +54 -0
  231. data/spec/support/harness/evaluate.rb +12 -1
  232. data/spec/support/harness/experiments.rb +1 -1
  233. data/spec/support/harness/fake_provider.rb +22 -6
  234. data/spec/support/harness/fault_preload.rb +3 -1
  235. data/spec/support/harness/mission_console.rb +54 -0
  236. data/spec/support/harness/mission_curses_pty.rb +41 -0
  237. data/spec/support/harness/mission_dispatch.rb +30 -0
  238. data/spec/support/harness/mission_fixture_preload.rb +24 -0
  239. data/spec/support/harness/mission_job_lifetime.rb +25 -0
  240. data/spec/support/harness/mission_legacy.rb +25 -0
  241. data/spec/support/harness/mission_normal_exit.rb +34 -0
  242. data/spec/support/harness/mission_swarm.rb +23 -0
  243. data/spec/support/harness/mission_unsupported.rb +29 -0
  244. data/spec/support/harness/package_acceptance.rb +33 -5
  245. data/spec/support/harness/session_acceptance.rb +4 -4
  246. data/spec/support/harness/swarm_acceptance.rb +1 -1
  247. data/spec/support/harness/swarm_control.rb +44 -0
  248. data/spec/support/mission_namespace.rb +14 -0
  249. data/spec/support/mission_pipe_queue.rb +33 -0
  250. data/third_party/pwn_rdoc.jsonl +605 -19
  251. metadata +129 -16
  252. data/scripts/benchmark_policy.rb +0 -376
  253. data/scripts/build_gate.rb +0 -58
  254. data/spec/integration/build_gate_spec.rb +0 -73
@@ -4,13 +4,13 @@
4
4
  <!-- Generated by graphviz version 14.1.2 (0)
5
5
  -->
6
6
  <!-- Title: PWN_Reinforcement_Learning Pages: 1 -->
7
- <svg width="978pt" height="901pt"
8
- viewBox="0.00 0.00 978.00 901.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
7
+ <svg width="1020pt" height="901pt"
8
+ viewBox="0.00 0.00 1020.00 901.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
9
9
  <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(28.8 872.29)">
10
10
  <title>PWN_Reinforcement_Learning</title>
11
- <polygon fill="#0f172a" stroke="none" points="-28.8,28.8 -28.8,-872.29 948.8,-872.29 948.8,28.8 -28.8,28.8"/>
12
- <text xml:space="preserve" text-anchor="start" x="178" y="-821.49" font-family="sans-Serif" font-weight="bold" font-size="20.00" fill="#e2e8f0">PWN::AI::Agent &#45; Reward + Curriculum Reinforcement Loop</text>
13
- <text xml:space="preserve" text-anchor="start" x="272.5" y="-810.04" font-family="sans-Serif" font-size="11.00" fill="#94a3b8">live accounting · optional curriculum / weights · explicit routing&#45;policy evaluation</text>
11
+ <polygon fill="#0f172a" stroke="none" points="-28.8,28.8 -28.8,-872.29 990.8,-872.29 990.8,28.8 -28.8,28.8"/>
12
+ <text xml:space="preserve" text-anchor="start" x="199" y="-821.49" font-family="sans-Serif" font-weight="bold" font-size="20.00" fill="#e2e8f0">PWN::AI::Agent &#45; Reward + Curriculum Reinforcement Loop</text>
13
+ <text xml:space="preserve" text-anchor="start" x="293.5" y="-810.04" font-family="sans-Serif" font-size="11.00" fill="#94a3b8">live accounting · optional curriculum / weights · explicit routing&#45;policy evaluation</text>
14
14
  <g id="clust1" class="cluster">
15
15
  <title>cluster_live</title>
16
16
  <path fill="#1e293b" stroke="#334155" d="M20,-8C20,-8 286,-8 286,-8 292,-8 298,-14 298,-20 298,-20 298,-777.74 298,-777.74 298,-783.74 292,-789.74 286,-789.74 286,-789.74 20,-789.74 20,-789.74 14,-789.74 8,-783.74 8,-777.74 8,-777.74 8,-20 8,-20 8,-14 14,-8 20,-8"/>
@@ -18,13 +18,13 @@
18
18
  </g>
19
19
  <g id="clust2" class="cluster">
20
20
  <title>cluster_curriculum</title>
21
- <path fill="#1e293b" stroke="#94a3b8" stroke-dasharray="5,2" d="M324,-8C324,-8 602,-8 602,-8 608,-8 614,-14 614,-20 614,-20 614,-784.49 614,-784.49 614,-790.49 608,-796.49 602,-796.49 602,-796.49 324,-796.49 324,-796.49 318,-796.49 312,-790.49 312,-784.49 312,-784.49 312,-20 312,-20 312,-14 318,-8 324,-8"/>
22
- <text xml:space="preserve" text-anchor="middle" x="463" y="-781.09" font-family="sans-Serif" font-size="12.00" fill="#e2e8f0">OPTIONAL CURRICULUM &#160;· &#160;independent invocation</text>
21
+ <path fill="#1e293b" stroke="#94a3b8" stroke-dasharray="5,2" d="M326,-8C326,-8 636,-8 636,-8 642,-8 648,-14 648,-20 648,-20 648,-784.49 648,-784.49 648,-790.49 642,-796.49 636,-796.49 636,-796.49 326,-796.49 326,-796.49 320,-796.49 314,-790.49 314,-784.49 314,-784.49 314,-20 314,-20 314,-14 320,-8 326,-8"/>
22
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-781.09" font-family="sans-Serif" font-size="12.00" fill="#e2e8f0">OPTIONAL CURRICULUM &#160;· &#160;independent invocation</text>
23
23
  </g>
24
24
  <g id="clust3" class="cluster">
25
25
  <title>cluster_policy</title>
26
- <path fill="#1e293b" stroke="#94a3b8" stroke-dasharray="5,2" d="M634,-142.29C634,-142.29 900,-142.29 900,-142.29 906,-142.29 912,-148.29 912,-154.29 912,-154.29 912,-777.74 912,-777.74 912,-783.74 906,-789.74 900,-789.74 900,-789.74 634,-789.74 634,-789.74 628,-789.74 622,-783.74 622,-777.74 622,-777.74 622,-154.29 622,-154.29 622,-148.29 628,-142.29 634,-142.29"/>
27
- <text xml:space="preserve" text-anchor="middle" x="767" y="-774.34" font-family="sans-Serif" font-size="12.00" fill="#e2e8f0">FROZEN POLICY &#160;· &#160;explicit operator workflow</text>
26
+ <path fill="#1e293b" stroke="#94a3b8" stroke-dasharray="5,2" d="M676,-142.29C676,-142.29 942,-142.29 942,-142.29 948,-142.29 954,-148.29 954,-154.29 954,-154.29 954,-777.74 954,-777.74 954,-783.74 948,-789.74 942,-789.74 942,-789.74 676,-789.74 676,-789.74 670,-789.74 664,-783.74 664,-777.74 664,-777.74 664,-154.29 664,-154.29 664,-148.29 670,-142.29 676,-142.29"/>
27
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-774.34" font-family="sans-Serif" font-size="12.00" fill="#e2e8f0">FROZEN POLICY &#160;· &#160;explicit operator workflow</text>
28
28
  </g>
29
29
  <!-- Run -->
30
30
  <g id="node1" class="node">
@@ -117,158 +117,158 @@
117
117
  <!-- Schedule -->
118
118
  <g id="node7" class="node">
119
119
  <title>Schedule</title>
120
- <path fill="#7dd3fc" stroke="#334155" stroke-width="1.3" d="M583.8,-766.24C583.8,-766.24 334.2,-766.24 334.2,-766.24 328.2,-766.24 322.2,-760.24 322.2,-754.24 322.2,-754.24 322.2,-687.7 322.2,-687.7 322.2,-681.7 328.2,-675.7 334.2,-675.7 334.2,-675.7 583.8,-675.7 583.8,-675.7 589.8,-675.7 595.8,-681.7 595.8,-687.7 595.8,-687.7 595.8,-754.24 595.8,-754.24 595.8,-760.24 589.8,-766.24 583.8,-766.24"/>
121
- <text xml:space="preserve" text-anchor="middle" x="459" y="-744.27" font-family="sans-Serif" font-size="11.00" fill="#0f172a">A &#160;OPT IN / INVOKE EXPLICITLY</text>
122
- <text xml:space="preserve" text-anchor="middle" x="459" y="-730.77" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Cron seeds disabled by default:</text>
123
- <text xml:space="preserve" text-anchor="middle" x="459" y="-717.27" font-family="sans-Serif" font-size="11.00" fill="#0f172a">practice 03:00; offline_judge 03:30 daily</text>
124
- <text xml:space="preserve" text-anchor="middle" x="459" y="-703.77" font-family="sans-Serif" font-size="11.00" fill="#0f172a">train dry_run 04:00 Sunday</text>
125
- <text xml:space="preserve" text-anchor="middle" x="459" y="-690.27" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Seeding jobs does not install system crontab</text>
120
+ <path fill="#7dd3fc" stroke="#334155" stroke-width="1.3" d="M628.15,-766.24C628.15,-766.24 333.85,-766.24 333.85,-766.24 327.85,-766.24 321.85,-760.24 321.85,-754.24 321.85,-754.24 321.85,-687.7 321.85,-687.7 321.85,-681.7 327.85,-675.7 333.85,-675.7 333.85,-675.7 628.15,-675.7 628.15,-675.7 634.15,-675.7 640.15,-681.7 640.15,-687.7 640.15,-687.7 640.15,-754.24 640.15,-754.24 640.15,-760.24 634.15,-766.24 628.15,-766.24"/>
121
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-744.27" font-family="sans-Serif" font-size="11.00" fill="#0f172a">A &#160;OPT IN / INVOKE EXPLICITLY</text>
122
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-730.77" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Curriculum jobs are not seeded</text>
123
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-717.27" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Create a CURRICULUM_JOBS template only when wanted</text>
124
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-703.77" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Templates stay disabled until explicitly enabled</text>
125
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-690.27" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Seeding does not install system crontab</text>
126
126
  </g>
127
127
  <!-- Practice -->
128
128
  <g id="node8" class="node">
129
129
  <title>Practice</title>
130
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.3" d="M583.8,-627.7C583.8,-627.7 334.2,-627.7 334.2,-627.7 328.2,-627.7 322.2,-621.7 322.2,-615.7 322.2,-615.7 322.2,-549.16 322.2,-549.16 322.2,-543.16 328.2,-537.16 334.2,-537.16 334.2,-537.16 583.8,-537.16 583.8,-537.16 589.8,-537.16 595.8,-543.16 595.8,-549.16 595.8,-549.16 595.8,-615.7 595.8,-615.7 595.8,-621.7 589.8,-627.7 583.8,-627.7"/>
131
- <text xml:space="preserve" text-anchor="middle" x="459" y="-605.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">B &#160;PRACTICE / OFFLINE JUDGING</text>
132
- <text xml:space="preserve" text-anchor="middle" x="459" y="-592.23" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Practice: budget&#45;first; real tools may run</text>
133
- <text xml:space="preserve" text-anchor="middle" x="459" y="-578.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Judge&#45;qualified resolution + winning traces</text>
134
- <text xml:space="preserve" text-anchor="middle" x="459" y="-565.23" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Offline judge: backfill, calibrate, warm sentinel</text>
135
- <text xml:space="preserve" text-anchor="middle" x="459" y="-551.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Records: curriculum/ + mistakes / preferences</text>
130
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.3" d="M605.8,-627.7C605.8,-627.7 356.2,-627.7 356.2,-627.7 350.2,-627.7 344.2,-621.7 344.2,-615.7 344.2,-615.7 344.2,-549.16 344.2,-549.16 344.2,-543.16 350.2,-537.16 356.2,-537.16 356.2,-537.16 605.8,-537.16 605.8,-537.16 611.8,-537.16 617.8,-543.16 617.8,-549.16 617.8,-549.16 617.8,-615.7 617.8,-615.7 617.8,-621.7 611.8,-627.7 605.8,-627.7"/>
131
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-605.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">B &#160;PRACTICE / OFFLINE JUDGING</text>
132
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-592.23" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Practice: budget&#45;first; real tools may run</text>
133
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-578.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Judge&#45;qualified resolution + winning traces</text>
134
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-565.23" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Offline judge: backfill, calibrate, warm sentinel</text>
135
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-551.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Records: curriculum/ + mistakes / preferences</text>
136
136
  </g>
137
137
  <!-- Schedule&#45;&gt;Practice -->
138
138
  <g id="edge6" class="edge">
139
139
  <title>Schedule&#45;&gt;Practice</title>
140
- <path fill="none" stroke="#94a3b8" stroke-width="1.3" stroke-dasharray="5,2" d="M459,-675.26C459,-663.36 459,-650.36 459,-637.99"/>
141
- <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="461.8,-638.21 459,-630.21 456.2,-638.21 461.8,-638.21"/>
142
- <text xml:space="preserve" text-anchor="middle" x="530.62" y="-648.2" font-family="sans-Serif" font-size="10.00" fill="#cbd5e1">independent jobs; not a job chain</text>
140
+ <path fill="none" stroke="#94a3b8" stroke-width="1.3" stroke-dasharray="5,2" d="M481,-675.26C481,-663.36 481,-650.36 481,-637.99"/>
141
+ <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="483.8,-638.21 481,-630.21 478.2,-638.21 483.8,-638.21"/>
142
+ <text xml:space="preserve" text-anchor="middle" x="552.62" y="-648.2" font-family="sans-Serif" font-size="10.00" fill="#cbd5e1">independent jobs; not a job chain</text>
143
143
  </g>
144
144
  <!-- Export -->
145
145
  <g id="node9" class="node">
146
146
  <title>Export</title>
147
- <path fill="#fcd34d" stroke="#334155" stroke-width="1.3" d="M583.8,-489.16C583.8,-489.16 334.2,-489.16 334.2,-489.16 328.2,-489.16 322.2,-483.16 322.2,-477.16 322.2,-477.16 322.2,-410.62 322.2,-410.62 322.2,-404.62 328.2,-398.62 334.2,-398.62 334.2,-398.62 583.8,-398.62 583.8,-398.62 589.8,-398.62 595.8,-404.62 595.8,-410.62 595.8,-410.62 595.8,-477.16 595.8,-477.16 595.8,-483.16 589.8,-489.16 583.8,-489.16"/>
148
- <text xml:space="preserve" text-anchor="middle" x="459" y="-467.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">C &#160;EXPORT TRAINING DATA</text>
149
- <text xml:space="preserve" text-anchor="middle" x="459" y="-453.69" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Learning.export_finetune + Reward.export_dpo</text>
150
- <text xml:space="preserve" text-anchor="middle" x="459" y="-440.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Eligible sessions / outcomes + preference pairs</text>
151
- <text xml:space="preserve" text-anchor="middle" x="459" y="-426.69" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Scrub, geometry and source&#45;balance filters</text>
152
- <text xml:space="preserve" text-anchor="middle" x="459" y="-413.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Output: finetune/*.jsonl + evaluation set</text>
147
+ <path fill="#fcd34d" stroke="#334155" stroke-width="1.3" d="M605.8,-489.16C605.8,-489.16 356.2,-489.16 356.2,-489.16 350.2,-489.16 344.2,-483.16 344.2,-477.16 344.2,-477.16 344.2,-410.62 344.2,-410.62 344.2,-404.62 350.2,-398.62 356.2,-398.62 356.2,-398.62 605.8,-398.62 605.8,-398.62 611.8,-398.62 617.8,-404.62 617.8,-410.62 617.8,-410.62 617.8,-477.16 617.8,-477.16 617.8,-483.16 611.8,-489.16 605.8,-489.16"/>
148
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-467.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">C &#160;EXPORT TRAINING DATA</text>
149
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-453.69" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Learning.export_finetune + Reward.export_dpo</text>
150
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-440.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Eligible sessions / outcomes + preference pairs</text>
151
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-426.69" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Scrub, geometry and source&#45;balance filters</text>
152
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-413.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Output: finetune/*.jsonl + evaluation set</text>
153
153
  </g>
154
154
  <!-- Practice&#45;&gt;Export -->
155
155
  <g id="edge7" class="edge">
156
156
  <title>Practice&#45;&gt;Export</title>
157
- <path fill="none" stroke="#94a3b8" stroke-width="1.3" stroke-dasharray="5,2" d="M459,-536.72C459,-524.82 459,-511.82 459,-499.45"/>
158
- <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="461.8,-499.67 459,-491.67 456.2,-499.67 461.8,-499.67"/>
159
- <text xml:space="preserve" text-anchor="middle" x="503.62" y="-509.66" font-family="sans-Serif" font-size="10.00" fill="#cbd5e1">eligible retained data</text>
157
+ <path fill="none" stroke="#94a3b8" stroke-width="1.3" stroke-dasharray="5,2" d="M481,-536.72C481,-524.82 481,-511.82 481,-499.45"/>
158
+ <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="483.8,-499.67 481,-491.67 478.2,-499.67 483.8,-499.67"/>
159
+ <text xml:space="preserve" text-anchor="middle" x="525.62" y="-509.66" font-family="sans-Serif" font-size="10.00" fill="#cbd5e1">eligible retained data</text>
160
160
  </g>
161
161
  <!-- Train -->
162
162
  <g id="node10" class="node">
163
163
  <title>Train</title>
164
- <path fill="#fda4af" stroke="#334155" stroke-width="1.3" d="M583.8,-361.62C583.8,-361.62 334.2,-361.62 334.2,-361.62 328.2,-361.62 322.2,-355.62 322.2,-349.62 322.2,-349.62 322.2,-283.08 322.2,-283.08 322.2,-277.08 328.2,-271.08 334.2,-271.08 334.2,-271.08 583.8,-271.08 583.8,-271.08 589.8,-271.08 595.8,-277.08 595.8,-283.08 595.8,-283.08 595.8,-349.62 595.8,-349.62 595.8,-355.62 589.8,-361.62 583.8,-361.62"/>
165
- <text xml:space="preserve" text-anchor="middle" x="459" y="-339.65" font-family="sans-Serif" font-size="11.00" fill="#0f172a">D &#160;train_and_gate</text>
166
- <text xml:space="preserve" text-anchor="middle" x="459" y="-326.15" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Dry run or no trainer → export_ready only</text>
167
- <text xml:space="preserve" text-anchor="middle" x="459" y="-312.65" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Otherwise: configured trainer → LoRA adapter</text>
168
- <text xml:space="preserve" text-anchor="middle" x="459" y="-299.15" font-family="sans-Serif" font-size="11.00" fill="#0f172a">ollama create → candidate vs baseline gate</text>
169
- <text xml:space="preserve" text-anchor="middle" x="459" y="-285.65" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Preference&#45;diet gate also required</text>
164
+ <path fill="#fda4af" stroke="#334155" stroke-width="1.3" d="M605.8,-361.62C605.8,-361.62 356.2,-361.62 356.2,-361.62 350.2,-361.62 344.2,-355.62 344.2,-349.62 344.2,-349.62 344.2,-283.08 344.2,-283.08 344.2,-277.08 350.2,-271.08 356.2,-271.08 356.2,-271.08 605.8,-271.08 605.8,-271.08 611.8,-271.08 617.8,-277.08 617.8,-283.08 617.8,-283.08 617.8,-349.62 617.8,-349.62 617.8,-355.62 611.8,-361.62 605.8,-361.62"/>
165
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-339.65" font-family="sans-Serif" font-size="11.00" fill="#0f172a">D &#160;train_and_gate</text>
166
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-326.15" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Dry run or no trainer → export_ready only</text>
167
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-312.65" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Otherwise: configured trainer → LoRA adapter</text>
168
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-299.15" font-family="sans-Serif" font-size="11.00" fill="#0f172a">ollama create → candidate vs baseline gate</text>
169
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-285.65" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Preference&#45;diet gate also required</text>
170
170
  </g>
171
171
  <!-- Export&#45;&gt;Train -->
172
172
  <g id="edge8" class="edge">
173
173
  <title>Export&#45;&gt;Train</title>
174
- <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M459,-398.28C459,-389.78 459,-380.79 459,-372.02"/>
175
- <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="461.8,-372.21 459,-364.21 456.2,-372.21 461.8,-372.21"/>
174
+ <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M481,-398.28C481,-389.78 481,-380.79 481,-372.02"/>
175
+ <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="483.8,-372.21 481,-364.21 478.2,-372.21 483.8,-372.21"/>
176
176
  </g>
177
177
  <!-- Model -->
178
178
  <g id="node11" class="node">
179
179
  <title>Model</title>
180
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.3" d="M583.8,-227.33C583.8,-227.33 334.2,-227.33 334.2,-227.33 328.2,-227.33 322.2,-221.33 322.2,-215.33 322.2,-215.33 322.2,-162.29 322.2,-162.29 322.2,-156.29 328.2,-150.29 334.2,-150.29 334.2,-150.29 583.8,-150.29 583.8,-150.29 589.8,-150.29 595.8,-156.29 595.8,-162.29 595.8,-162.29 595.8,-215.33 595.8,-215.33 595.8,-221.33 589.8,-227.33 583.8,-227.33"/>
181
- <text xml:space="preserve" text-anchor="middle" x="459" y="-205.36" font-family="sans-Serif" font-size="11.00" fill="#0f172a">E &#160;RETAIN GATED ADAPTER VERSION</text>
182
- <text xml:space="preserve" text-anchor="middle" x="459" y="-191.86" font-family="sans-Serif" font-size="11.00" fill="#0f172a">On passing gates: update curriculum model state</text>
183
- <text xml:space="preserve" text-anchor="middle" x="459" y="-178.36" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Optional local weight path, not routing policy</text>
184
- <text xml:space="preserve" text-anchor="middle" x="459" y="-164.86" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Export alone is not training or improvement</text>
180
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.3" d="M605.8,-227.33C605.8,-227.33 356.2,-227.33 356.2,-227.33 350.2,-227.33 344.2,-221.33 344.2,-215.33 344.2,-215.33 344.2,-162.29 344.2,-162.29 344.2,-156.29 350.2,-150.29 356.2,-150.29 356.2,-150.29 605.8,-150.29 605.8,-150.29 611.8,-150.29 617.8,-156.29 617.8,-162.29 617.8,-162.29 617.8,-215.33 617.8,-215.33 617.8,-221.33 611.8,-227.33 605.8,-227.33"/>
181
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-205.36" font-family="sans-Serif" font-size="11.00" fill="#0f172a">E &#160;RETAIN GATED ADAPTER VERSION</text>
182
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-191.86" font-family="sans-Serif" font-size="11.00" fill="#0f172a">On passing gates: update curriculum model state</text>
183
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-178.36" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Optional local weight path, not routing policy</text>
184
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-164.86" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Export alone is not training or improvement</text>
185
185
  </g>
186
186
  <!-- Train&#45;&gt;Model -->
187
187
  <g id="edge9" class="edge">
188
188
  <title>Train&#45;&gt;Model</title>
189
- <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M459,-270.74C459,-259.94 459,-248.37 459,-237.44"/>
190
- <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="461.8,-237.6 459,-229.6 456.2,-237.6 461.8,-237.6"/>
189
+ <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M481,-270.74C481,-259.94 481,-248.37 481,-237.44"/>
190
+ <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="483.8,-237.6 481,-229.6 478.2,-237.6 483.8,-237.6"/>
191
191
  </g>
192
192
  <!-- Hygiene -->
193
193
  <g id="node12" class="node">
194
194
  <title>Hygiene</title>
195
- <path fill="#1e293b" stroke="#334155" stroke-width="1.3" stroke-dasharray="5,2" d="M587.8,-106.54C587.8,-106.54 338.2,-106.54 338.2,-106.54 332.2,-106.54 326.2,-100.54 326.2,-94.54 326.2,-94.54 326.2,-28 326.2,-28 326.2,-22 332.2,-16 338.2,-16 338.2,-16 587.8,-16 587.8,-16 593.8,-16 599.8,-22 599.8,-28 599.8,-28 599.8,-94.54 599.8,-94.54 599.8,-100.54 593.8,-106.54 587.8,-106.54"/>
196
- <text xml:space="preserve" text-anchor="middle" x="463" y="-84.57" font-family="sans-Serif" font-size="11.00" fill="#cbd5e1">SEPARATE ENABLED HYGIENE SEED</text>
197
- <text xml:space="preserve" text-anchor="middle" x="463" y="-71.07" font-family="sans-Serif" font-size="11.00" fill="#cbd5e1">Learning.consolidate at 05:00 daily</text>
198
- <text xml:space="preserve" text-anchor="middle" x="463" y="-57.57" font-family="sans-Serif" font-size="11.00" fill="#cbd5e1">Memory maintenance, not model training</text>
199
- <text xml:space="preserve" text-anchor="middle" x="463" y="-44.07" font-family="sans-Serif" font-size="11.00" fill="#cbd5e1">ESR = verified_exploit_tools / vulnerable_tools</text>
200
- <text xml:space="preserve" text-anchor="middle" x="463" y="-30.57" font-family="sans-Serif" font-size="11.00" fill="#cbd5e1">ASR = successful_attacks / total_attack_attempts</text>
195
+ <path fill="#1e293b" stroke="#334155" stroke-width="1.3" stroke-dasharray="5,2" d="M605.8,-106.54C605.8,-106.54 356.2,-106.54 356.2,-106.54 350.2,-106.54 344.2,-100.54 344.2,-94.54 344.2,-94.54 344.2,-28 344.2,-28 344.2,-22 350.2,-16 356.2,-16 356.2,-16 605.8,-16 605.8,-16 611.8,-16 617.8,-22 617.8,-28 617.8,-28 617.8,-94.54 617.8,-94.54 617.8,-100.54 611.8,-106.54 605.8,-106.54"/>
196
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-84.57" font-family="sans-Serif" font-size="11.00" fill="#cbd5e1">SEPARATE ENABLED HYGIENE SEED</text>
197
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-71.07" font-family="sans-Serif" font-size="11.00" fill="#cbd5e1">session_cleanup at 03:45 daily</text>
198
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-57.57" font-family="sans-Serif" font-size="11.00" fill="#cbd5e1">Includes memory consolidation; not model training</text>
199
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-44.07" font-family="sans-Serif" font-size="11.00" fill="#cbd5e1">ESR = verified_exploit_tools / vulnerable_tools</text>
200
+ <text xml:space="preserve" text-anchor="middle" x="481" y="-30.57" font-family="sans-Serif" font-size="11.00" fill="#cbd5e1">ASR = successful_attacks / total_attack_attempts</text>
201
201
  </g>
202
202
  <!-- Select -->
203
203
  <g id="node13" class="node">
204
204
  <title>Select</title>
205
- <path fill="#c4b5fd" stroke="#334155" stroke-width="1.3" d="M891.8,-759.49C891.8,-759.49 642.2,-759.49 642.2,-759.49 636.2,-759.49 630.2,-753.49 630.2,-747.49 630.2,-747.49 630.2,-694.45 630.2,-694.45 630.2,-688.45 636.2,-682.45 642.2,-682.45 642.2,-682.45 891.8,-682.45 891.8,-682.45 897.8,-682.45 903.8,-688.45 903.8,-694.45 903.8,-694.45 903.8,-747.49 903.8,-747.49 903.8,-753.49 897.8,-759.49 891.8,-759.49"/>
206
- <text xml:space="preserve" text-anchor="middle" x="767" y="-737.52" font-family="sans-Serif" font-size="11.00" fill="#0f172a">I &#160;SELECT SNAPSHOTS</text>
207
- <text xml:space="preserve" text-anchor="middle" x="767" y="-724.02" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Explicit baseline + candidate policy bytes</text>
208
- <text xml:space="preserve" text-anchor="middle" x="767" y="-710.52" font-family="sans-Serif" font-size="11.00" fill="#0f172a">PolicyEvaluation; not live online writers</text>
209
- <text xml:space="preserve" text-anchor="middle" x="767" y="-697.02" font-family="sans-Serif" font-size="11.00" fill="#0f172a">No automatic handoff from RSI or training</text>
205
+ <path fill="#c4b5fd" stroke="#334155" stroke-width="1.3" d="M933.8,-759.49C933.8,-759.49 684.2,-759.49 684.2,-759.49 678.2,-759.49 672.2,-753.49 672.2,-747.49 672.2,-747.49 672.2,-694.45 672.2,-694.45 672.2,-688.45 678.2,-682.45 684.2,-682.45 684.2,-682.45 933.8,-682.45 933.8,-682.45 939.8,-682.45 945.8,-688.45 945.8,-694.45 945.8,-694.45 945.8,-747.49 945.8,-747.49 945.8,-753.49 939.8,-759.49 933.8,-759.49"/>
206
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-737.52" font-family="sans-Serif" font-size="11.00" fill="#0f172a">I &#160;SELECT SNAPSHOTS</text>
207
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-724.02" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Explicit baseline + candidate policy bytes</text>
208
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-710.52" font-family="sans-Serif" font-size="11.00" fill="#0f172a">PolicyEvaluation; not live online writers</text>
209
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-697.02" font-family="sans-Serif" font-size="11.00" fill="#0f172a">No automatic handoff from RSI or training</text>
210
210
  </g>
211
211
  <!-- Eval -->
212
212
  <g id="node14" class="node">
213
213
  <title>Eval</title>
214
- <path fill="#c4b5fd" stroke="#334155" stroke-width="1.3" d="M891.8,-627.7C891.8,-627.7 642.2,-627.7 642.2,-627.7 636.2,-627.7 630.2,-621.7 630.2,-615.7 630.2,-615.7 630.2,-549.16 630.2,-549.16 630.2,-543.16 636.2,-537.16 642.2,-537.16 642.2,-537.16 891.8,-537.16 891.8,-537.16 897.8,-537.16 903.8,-543.16 903.8,-549.16 903.8,-549.16 903.8,-615.7 903.8,-615.7 903.8,-621.7 897.8,-627.7 891.8,-627.7"/>
215
- <text xml:space="preserve" text-anchor="middle" x="767" y="-605.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">II &#160;EVALUATE LOCALLY</text>
216
- <text xml:space="preserve" text-anchor="middle" x="767" y="-592.23" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Public fixed held&#45;out tasks</text>
217
- <text xml:space="preserve" text-anchor="middle" x="767" y="-578.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Off / baseline / candidate comparisons</text>
218
- <text xml:space="preserve" text-anchor="middle" x="767" y="-565.23" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Two disposable filesystem environments</text>
219
- <text xml:space="preserve" text-anchor="middle" x="767" y="-551.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Regression replay is a separate split</text>
214
+ <path fill="#c4b5fd" stroke="#334155" stroke-width="1.3" d="M933.8,-627.7C933.8,-627.7 684.2,-627.7 684.2,-627.7 678.2,-627.7 672.2,-621.7 672.2,-615.7 672.2,-615.7 672.2,-549.16 672.2,-549.16 672.2,-543.16 678.2,-537.16 684.2,-537.16 684.2,-537.16 933.8,-537.16 933.8,-537.16 939.8,-537.16 945.8,-543.16 945.8,-549.16 945.8,-549.16 945.8,-615.7 945.8,-615.7 945.8,-621.7 939.8,-627.7 933.8,-627.7"/>
215
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-605.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">II &#160;EVALUATE LOCALLY</text>
216
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-592.23" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Public fixed held&#45;out tasks</text>
217
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-578.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Off / baseline / candidate comparisons</text>
218
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-565.23" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Two disposable filesystem environments</text>
219
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-551.73" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Regression replay is a separate split</text>
220
220
  </g>
221
221
  <!-- Select&#45;&gt;Eval -->
222
222
  <g id="edge10" class="edge">
223
223
  <title>Select&#45;&gt;Eval</title>
224
- <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M767,-681.94C767,-668.21 767,-652.45 767,-637.61"/>
225
- <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="769.8,-637.99 767,-629.99 764.2,-637.99 769.8,-637.99"/>
224
+ <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M809,-681.94C809,-668.21 809,-652.45 809,-637.61"/>
225
+ <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="811.8,-637.99 809,-629.99 806.2,-637.99 811.8,-637.99"/>
226
226
  </g>
227
227
  <!-- Gate -->
228
228
  <g id="node15" class="node">
229
229
  <title>Gate</title>
230
- <path fill="#fcd34d" stroke="#334155" stroke-width="1.3" d="M891.8,-489.16C891.8,-489.16 642.2,-489.16 642.2,-489.16 636.2,-489.16 630.2,-483.16 630.2,-477.16 630.2,-477.16 630.2,-410.62 630.2,-410.62 630.2,-404.62 636.2,-398.62 642.2,-398.62 642.2,-398.62 891.8,-398.62 891.8,-398.62 897.8,-398.62 903.8,-404.62 903.8,-410.62 903.8,-410.62 903.8,-477.16 903.8,-477.16 903.8,-483.16 897.8,-489.16 891.8,-489.16"/>
231
- <text xml:space="preserve" text-anchor="middle" x="767" y="-467.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">III &#160;REPLAY ACCEPTANCE</text>
232
- <text xml:space="preserve" text-anchor="middle" x="767" y="-453.69" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Fresh replay + aggregate nonregression</text>
233
- <text xml:space="preserve" text-anchor="middle" x="767" y="-440.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Per&#45;task nonregression + held&#45;out gain</text>
234
- <text xml:space="preserve" text-anchor="middle" x="767" y="-426.69" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Local controller evidence only</text>
235
- <text xml:space="preserve" text-anchor="middle" x="767" y="-413.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Not proof of general live LLM gains</text>
230
+ <path fill="#fcd34d" stroke="#334155" stroke-width="1.3" d="M933.8,-489.16C933.8,-489.16 684.2,-489.16 684.2,-489.16 678.2,-489.16 672.2,-483.16 672.2,-477.16 672.2,-477.16 672.2,-410.62 672.2,-410.62 672.2,-404.62 678.2,-398.62 684.2,-398.62 684.2,-398.62 933.8,-398.62 933.8,-398.62 939.8,-398.62 945.8,-404.62 945.8,-410.62 945.8,-410.62 945.8,-477.16 945.8,-477.16 945.8,-483.16 939.8,-489.16 933.8,-489.16"/>
231
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-467.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">III &#160;REPLAY ACCEPTANCE</text>
232
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-453.69" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Fresh replay + aggregate nonregression</text>
233
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-440.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Per&#45;task nonregression + held&#45;out gain</text>
234
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-426.69" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Local controller evidence only</text>
235
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-413.19" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Not proof of general live LLM gains</text>
236
236
  </g>
237
237
  <!-- Eval&#45;&gt;Gate -->
238
238
  <g id="edge11" class="edge">
239
239
  <title>Eval&#45;&gt;Gate</title>
240
- <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M767,-536.72C767,-524.82 767,-511.82 767,-499.45"/>
241
- <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="769.8,-499.67 767,-491.67 764.2,-499.67 769.8,-499.67"/>
240
+ <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M809,-536.72C809,-524.82 809,-511.82 809,-499.45"/>
241
+ <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="811.8,-499.67 809,-491.67 806.2,-499.67 811.8,-499.67"/>
242
242
  </g>
243
243
  <!-- Approve -->
244
244
  <g id="node16" class="node">
245
245
  <title>Approve</title>
246
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.3" d="M891.8,-354.87C891.8,-354.87 642.2,-354.87 642.2,-354.87 636.2,-354.87 630.2,-348.87 630.2,-342.87 630.2,-342.87 630.2,-289.83 630.2,-289.83 630.2,-283.83 636.2,-277.83 642.2,-277.83 642.2,-277.83 891.8,-277.83 891.8,-277.83 897.8,-277.83 903.8,-283.83 903.8,-289.83 903.8,-289.83 903.8,-342.87 903.8,-342.87 903.8,-348.87 897.8,-354.87 891.8,-354.87"/>
247
- <text xml:space="preserve" text-anchor="middle" x="767" y="-332.9" font-family="sans-Serif" font-size="11.00" fill="#0f172a">IV &#160;OPERATOR APPROVAL</text>
248
- <text xml:space="preserve" text-anchor="middle" x="767" y="-319.4" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Explicit live policy target</text>
249
- <text xml:space="preserve" text-anchor="middle" x="767" y="-305.9" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Approval + stopped writers required</text>
250
- <text xml:space="preserve" text-anchor="middle" x="767" y="-292.4" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Replace routing policy, not model weights</text>
246
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.3" d="M933.8,-354.87C933.8,-354.87 684.2,-354.87 684.2,-354.87 678.2,-354.87 672.2,-348.87 672.2,-342.87 672.2,-342.87 672.2,-289.83 672.2,-289.83 672.2,-283.83 678.2,-277.83 684.2,-277.83 684.2,-277.83 933.8,-277.83 933.8,-277.83 939.8,-277.83 945.8,-283.83 945.8,-289.83 945.8,-289.83 945.8,-342.87 945.8,-342.87 945.8,-348.87 939.8,-354.87 933.8,-354.87"/>
247
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-332.9" font-family="sans-Serif" font-size="11.00" fill="#0f172a">IV &#160;OPERATOR APPROVAL</text>
248
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-319.4" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Explicit live policy target</text>
249
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-305.9" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Approval + stopped writers required</text>
250
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-292.4" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Replace routing policy, not model weights</text>
251
251
  </g>
252
252
  <!-- Gate&#45;&gt;Approve -->
253
253
  <g id="edge12" class="edge">
254
254
  <title>Gate&#45;&gt;Approve</title>
255
- <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M767,-398.28C767,-387.48 767,-375.91 767,-364.98"/>
256
- <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="769.8,-365.14 767,-357.14 764.2,-365.14 769.8,-365.14"/>
255
+ <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M809,-398.28C809,-387.48 809,-375.91 809,-364.98"/>
256
+ <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="811.8,-365.14 809,-357.14 806.2,-365.14 811.8,-365.14"/>
257
257
  </g>
258
258
  <!-- Receipt -->
259
259
  <g id="node17" class="node">
260
260
  <title>Receipt</title>
261
- <path fill="#fcd34d" stroke="#334155" stroke-width="1.3" d="M891.8,-227.33C891.8,-227.33 642.2,-227.33 642.2,-227.33 636.2,-227.33 630.2,-221.33 630.2,-215.33 630.2,-215.33 630.2,-162.29 630.2,-162.29 630.2,-156.29 636.2,-150.29 642.2,-150.29 642.2,-150.29 891.8,-150.29 891.8,-150.29 897.8,-150.29 903.8,-156.29 903.8,-162.29 903.8,-162.29 903.8,-215.33 903.8,-215.33 903.8,-221.33 897.8,-227.33 891.8,-227.33"/>
262
- <text xml:space="preserve" text-anchor="middle" x="767" y="-205.36" font-family="sans-Serif" font-size="11.00" fill="#0f172a">V &#160;RETAIN RECEIPT / ROLLBACK</text>
263
- <text xml:space="preserve" text-anchor="middle" x="767" y="-191.86" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Versioned receipt v1 + byte digests</text>
264
- <text xml:space="preserve" text-anchor="middle" x="767" y="-178.36" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Rollback refuses intervening live writes</text>
265
- <text xml:space="preserve" text-anchor="middle" x="767" y="-164.86" font-family="sans-Serif" font-size="11.00" fill="#0f172a">No silent overwrite of newer policy state</text>
261
+ <path fill="#fcd34d" stroke="#334155" stroke-width="1.3" d="M933.8,-227.33C933.8,-227.33 684.2,-227.33 684.2,-227.33 678.2,-227.33 672.2,-221.33 672.2,-215.33 672.2,-215.33 672.2,-162.29 672.2,-162.29 672.2,-156.29 678.2,-150.29 684.2,-150.29 684.2,-150.29 933.8,-150.29 933.8,-150.29 939.8,-150.29 945.8,-156.29 945.8,-162.29 945.8,-162.29 945.8,-215.33 945.8,-215.33 945.8,-221.33 939.8,-227.33 933.8,-227.33"/>
262
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-205.36" font-family="sans-Serif" font-size="11.00" fill="#0f172a">V &#160;RETAIN RECEIPT / ROLLBACK</text>
263
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-191.86" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Versioned receipt v1 + byte digests</text>
264
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-178.36" font-family="sans-Serif" font-size="11.00" fill="#0f172a">Rollback refuses intervening live writes</text>
265
+ <text xml:space="preserve" text-anchor="middle" x="809" y="-164.86" font-family="sans-Serif" font-size="11.00" fill="#0f172a">No silent overwrite of newer policy state</text>
266
266
  </g>
267
267
  <!-- Approve&#45;&gt;Receipt -->
268
268
  <g id="edge13" class="edge">
269
269
  <title>Approve&#45;&gt;Receipt</title>
270
- <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M767,-277.34C767,-264.81 767,-250.7 767,-237.53"/>
271
- <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="769.8,-237.66 767,-229.66 764.2,-237.66 769.8,-237.66"/>
270
+ <path fill="none" stroke="#94a3b8" stroke-width="1.3" d="M809,-277.34C809,-264.81 809,-250.7 809,-237.53"/>
271
+ <polygon fill="#94a3b8" stroke="#94a3b8" stroke-width="1.3" points="811.8,-237.66 809,-229.66 806.2,-237.66 811.8,-237.66"/>
272
272
  </g>
273
273
  </g>
274
274
  </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.