pwn 0.5.783 → 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 (264) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +8 -8
  4. data/README.md +37 -0
  5. data/Rakefile +19 -1
  6. data/bin/pwn +2 -1
  7. data/bin/pwn_setup +1 -1
  8. data/bin/pwn_web_cache_deception +35 -7
  9. data/build_gem.sh +1 -1
  10. data/documentation/AI-Integration.md +7 -5
  11. data/documentation/Agent-Tool-Registry.md +2 -2
  12. data/documentation/Backup-Retention.md +82 -0
  13. data/documentation/Configuration.md +10 -6
  14. data/documentation/Contributing.md +1 -1
  15. data/documentation/Cron.md +64 -35
  16. data/documentation/Diagrams.md +2 -2
  17. data/documentation/General-PWN-Usage.md +4 -3
  18. data/documentation/Home.md +6 -5
  19. data/documentation/How-PWN-Works.md +7 -6
  20. data/documentation/Installation.md +254 -10
  21. data/documentation/Mission-Cancellation.md +134 -0
  22. data/documentation/Operability.md +17 -0
  23. data/documentation/Persistence.md +8 -6
  24. data/documentation/Plugin-Degradation.md +12 -0
  25. data/documentation/Plugins.md +3 -2
  26. data/documentation/Reinforcement-Learning.md +5 -6
  27. data/documentation/Runtime-Source-Protection.md +162 -0
  28. data/documentation/Transparent-Browser.md +11 -0
  29. data/documentation/Troubleshooting.md +3 -2
  30. data/documentation/What-is-PWN.md +2 -2
  31. data/documentation/YubiKey-Vaults.md +183 -0
  32. data/documentation/YubiKey.md +339 -0
  33. data/documentation/diagrams/cron-scheduling.svg +130 -128
  34. data/documentation/diagrams/dot/cron-scheduling.dot +5 -5
  35. data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
  36. data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
  37. data/documentation/diagrams/persistence-filesystem.svg +172 -142
  38. data/documentation/diagrams/plugin-ecosystem.svg +64 -64
  39. data/documentation/pwn-ai-Agent.md +10 -4
  40. data/etc/default_cron/session_cleanup.rb +1245 -22
  41. data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
  42. data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
  43. data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
  44. data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
  45. data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
  46. data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
  47. data/etc/default_skills/pwn/backups/SKILL.md +54 -0
  48. data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
  49. data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
  50. data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
  51. data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
  52. data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
  53. data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
  54. data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
  55. data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
  56. data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
  57. data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
  58. data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
  59. data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
  60. data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
  61. data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
  62. data/lib/pwn/ai/agent/dispatch.rb +26 -15
  63. data/lib/pwn/ai/agent/extrospection.rb +6 -7
  64. data/lib/pwn/ai/agent/learning.rb +62 -25
  65. data/lib/pwn/ai/agent/loop.rb +17 -1
  66. data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
  67. data/lib/pwn/ai/agent/policy.rb +5 -0
  68. data/lib/pwn/ai/agent/policy_evaluation.rb +8 -8
  69. data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
  70. data/lib/pwn/ai/agent/request_runtime.rb +122 -18
  71. data/lib/pwn/ai/agent/reward.rb +5 -3
  72. data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
  73. data/lib/pwn/ai/agent/skill_review.rb +9 -5
  74. data/lib/pwn/ai/agent/swarm.rb +35 -16
  75. data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
  76. data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
  77. data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
  78. data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
  79. data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
  80. data/lib/pwn/ai/agent.rb +1 -0
  81. data/lib/pwn/ai/anthropic.rb +8 -33
  82. data/lib/pwn/ai/cli.rb +34 -5
  83. data/lib/pwn/ai/gemini.rb +1 -0
  84. data/lib/pwn/ai/grok.rb +39 -28
  85. data/lib/pwn/ai/ollama.rb +4 -2
  86. data/lib/pwn/ai/open_ai.rb +11 -32
  87. data/lib/pwn/ai/open_web_ui.rb +62 -4
  88. data/lib/pwn/ai/router.rb +53 -3
  89. data/lib/pwn/ai/server_tools.rb +231 -0
  90. data/lib/pwn/ai.rb +1 -0
  91. data/lib/pwn/backups.rb +347 -0
  92. data/lib/pwn/config.rb +70 -39
  93. data/lib/pwn/cron.rb +120 -49
  94. data/lib/pwn/migrate.rb +150 -68
  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/binary_parser.rb +5 -4
  100. data/lib/pwn/plugins/ghidra.rb +4 -3
  101. data/lib/pwn/plugins/google_workspace.rb +4 -21
  102. data/lib/pwn/plugins/jobs.rb +1 -1
  103. data/lib/pwn/plugins/log.rb +21 -3
  104. data/lib/pwn/plugins/preflight_checker.rb +34 -9
  105. data/lib/pwn/plugins/recon.rb +4 -1
  106. data/lib/pwn/plugins/repl/ai.rb +305 -27
  107. data/lib/pwn/plugins/repl/ai_console.rb +273 -89
  108. data/lib/pwn/plugins/repl/ai_swarm.rb +181 -62
  109. data/lib/pwn/plugins/repl/mesh.rb +3 -16
  110. data/lib/pwn/plugins/repl/vault.rb +4 -15
  111. data/lib/pwn/plugins/repl.rb +1 -216
  112. data/lib/pwn/plugins/sandbox/driver.rb +4 -0
  113. data/lib/pwn/plugins/transparent_browser.rb +215 -0
  114. data/lib/pwn/plugins/vault.rb +412 -135
  115. data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
  116. data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
  117. data/lib/pwn/plugins/yubikey/management.rb +686 -0
  118. data/lib/pwn/plugins/yubikey/oath.rb +766 -0
  119. data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
  120. data/lib/pwn/plugins/yubikey/otp.rb +897 -0
  121. data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
  122. data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
  123. data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
  124. data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
  125. data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
  126. data/lib/pwn/plugins/yubikey.rb +844 -0
  127. data/lib/pwn/plugins.rb +1 -0
  128. data/lib/pwn/redaction.rb +69 -12
  129. data/lib/pwn/session_trace.rb +4 -0
  130. data/lib/pwn/sessions.rb +58 -16
  131. data/lib/pwn/setup.rb +123 -41
  132. data/lib/pwn/version.rb +1 -1
  133. data/lib/pwn/www/app_cobalt_io.rb +16 -5
  134. data/lib/pwn/www/bug_crowd.rb +16 -5
  135. data/lib/pwn/www/coinbase_pro.rb +16 -5
  136. data/lib/pwn/www/github.rb +30 -14
  137. data/lib/pwn/www/paypal.rb +15 -4
  138. data/lib/pwn/www/synack.rb +16 -5
  139. data/lib/pwn/www/twitter.rb +17 -6
  140. data/lib/pwn.rb +1 -0
  141. data/scripts/build_gate.rb +58 -0
  142. data/spec/documentation/installation_md_spec.rb +1 -1
  143. data/spec/fixtures/yubikey/apdu_frames.json +162 -0
  144. data/spec/integration/authentication_helper_oath_spec.rb +117 -0
  145. data/spec/integration/build_gate_spec.rb +73 -0
  146. data/spec/integration/controller_spill_spec.rb +48 -0
  147. data/spec/integration/harness_process_spec.rb +97 -0
  148. data/spec/integration/mission_cancel_spec.rb +79 -0
  149. data/spec/integration/mission_curses_pty_spec.rb +44 -0
  150. data/spec/integration/persistence_roundtrip_spec.rb +1 -7
  151. data/spec/integration/readme_security_spec.rb +47 -0
  152. data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
  153. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
  154. data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
  155. data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
  156. data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
  157. data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
  158. data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
  159. data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
  160. data/spec/lib/pwn/ai/agent/policy_spec.rb +27 -0
  161. data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
  162. data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
  163. data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
  164. data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
  165. data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
  166. data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
  167. data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
  168. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
  169. data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
  170. data/spec/lib/pwn/ai/cli_spec.rb +6 -9
  171. data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
  172. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
  173. data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
  174. data/spec/lib/pwn/ai/router_spec.rb +31 -3
  175. data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
  176. data/spec/lib/pwn/backups_spec.rb +114 -0
  177. data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
  178. data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
  179. data/spec/lib/pwn/cron_spec.rb +113 -6
  180. data/spec/lib/pwn/migrate_spec.rb +108 -5
  181. data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
  182. data/spec/lib/pwn/module_skills_spec.rb +8 -0
  183. data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
  184. data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
  185. data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
  186. data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
  187. data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
  188. data/spec/lib/pwn/plugins/log_spec.rb +7 -0
  189. data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
  190. data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
  191. data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
  192. data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
  193. data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +125 -78
  194. data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
  195. data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +30 -16
  196. data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
  197. data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
  198. data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
  199. data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
  200. data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
  201. data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
  202. data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
  203. data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
  204. data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
  205. data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
  206. data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
  207. data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
  208. data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
  209. data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
  210. data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
  211. data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
  212. data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
  213. data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
  214. data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
  215. data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
  216. data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
  217. data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
  218. data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
  219. data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
  220. data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
  221. data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
  222. data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
  223. data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
  224. data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
  225. data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
  226. data/spec/lib/pwn/redaction_spec.rb +13 -10
  227. data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
  228. data/spec/lib/pwn/session_cleanup_concurrency_spec.rb +201 -0
  229. data/spec/lib/pwn/session_cleanup_default_spec.rb +167 -2
  230. data/spec/lib/pwn/session_cleanup_legacy_spec.rb +101 -0
  231. data/spec/lib/pwn/session_cleanup_payload_spec.rb +335 -2
  232. data/spec/lib/pwn/session_cleanup_recovery_spec.rb +403 -0
  233. data/spec/lib/pwn/sessions_spec.rb +112 -0
  234. data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
  235. data/spec/lib/pwn/setup_spec.rb +3 -1
  236. data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
  237. data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
  238. data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
  239. data/spec/spec_helper.rb +15 -0
  240. data/spec/support/gate_failure_formatter.rb +14 -0
  241. data/spec/support/harness/cli_faults.rb +13 -9
  242. data/spec/support/harness/controller_spill.rb +26 -0
  243. data/spec/support/harness/controller_state.rb +54 -0
  244. data/spec/support/harness/evaluate.rb +12 -1
  245. data/spec/support/harness/experiments.rb +1 -1
  246. data/spec/support/harness/fake_provider.rb +22 -6
  247. data/spec/support/harness/fault_preload.rb +3 -1
  248. data/spec/support/harness/mission_console.rb +54 -0
  249. data/spec/support/harness/mission_curses_pty.rb +41 -0
  250. data/spec/support/harness/mission_dispatch.rb +30 -0
  251. data/spec/support/harness/mission_fixture_preload.rb +24 -0
  252. data/spec/support/harness/mission_job_lifetime.rb +25 -0
  253. data/spec/support/harness/mission_legacy.rb +25 -0
  254. data/spec/support/harness/mission_normal_exit.rb +34 -0
  255. data/spec/support/harness/mission_swarm.rb +23 -0
  256. data/spec/support/harness/mission_unsupported.rb +29 -0
  257. data/spec/support/harness/package_acceptance.rb +33 -5
  258. data/spec/support/harness/session_acceptance.rb +4 -4
  259. data/spec/support/harness/swarm_acceptance.rb +1 -1
  260. data/spec/support/harness/swarm_control.rb +44 -0
  261. data/spec/support/mission_namespace.rb +14 -0
  262. data/spec/support/mission_pipe_queue.rb +33 -0
  263. data/third_party/pwn_rdoc.jsonl +599 -15
  264. metadata +140 -17
@@ -0,0 +1,162 @@
1
+ {
2
+ "reference": "Yubico yubikey-manager 5.9.1 smartcard formatters; generated offline with synthetic bytes, not a hardware capture",
3
+ "source": "https://github.com/Yubico/yubikey-manager/blob/5.9.1/yubikit/core/smartcard/__init__.py",
4
+ "vectors": [
5
+ {
6
+ "name": "short-0-0",
7
+ "options": {
8
+ "cla": 0,
9
+ "ins": 202,
10
+ "p1": 1,
11
+ "p2": 2,
12
+ "data_hex": "",
13
+ "le": null,
14
+ "extended": false
15
+ },
16
+ "expected_hex": "00ca010200"
17
+ },
18
+ {
19
+ "name": "short-0-1",
20
+ "options": {
21
+ "cla": 0,
22
+ "ins": 202,
23
+ "p1": 1,
24
+ "p2": 2,
25
+ "data_hex": "",
26
+ "le": 1,
27
+ "extended": false
28
+ },
29
+ "expected_hex": "00ca010201"
30
+ },
31
+ {
32
+ "name": "short-1-0",
33
+ "options": {
34
+ "cla": 0,
35
+ "ins": 202,
36
+ "p1": 1,
37
+ "p2": 2,
38
+ "data_hex": "00",
39
+ "le": null,
40
+ "extended": false
41
+ },
42
+ "expected_hex": "00ca01020100"
43
+ },
44
+ {
45
+ "name": "short-16-16",
46
+ "options": {
47
+ "cla": 0,
48
+ "ins": 202,
49
+ "p1": 1,
50
+ "p2": 2,
51
+ "data_hex": "000102030405060708090a0b0c0d0e0f",
52
+ "le": 16,
53
+ "extended": false
54
+ },
55
+ "expected_hex": "00ca010210000102030405060708090a0b0c0d0e0f10"
56
+ },
57
+ {
58
+ "name": "short-255-0",
59
+ "options": {
60
+ "cla": 0,
61
+ "ins": 202,
62
+ "p1": 1,
63
+ "p2": 2,
64
+ "data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
65
+ "le": null,
66
+ "extended": false
67
+ },
68
+ "expected_hex": "00ca0102ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe"
69
+ },
70
+ {
71
+ "name": "short-255-255",
72
+ "options": {
73
+ "cla": 0,
74
+ "ins": 202,
75
+ "p1": 1,
76
+ "p2": 2,
77
+ "data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
78
+ "le": 255,
79
+ "extended": false
80
+ },
81
+ "expected_hex": "00ca0102ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
82
+ },
83
+ {
84
+ "name": "extended-0-0",
85
+ "options": {
86
+ "cla": 0,
87
+ "ins": 202,
88
+ "p1": 1,
89
+ "p2": 2,
90
+ "data_hex": "",
91
+ "le": null,
92
+ "extended": true
93
+ },
94
+ "expected_hex": "00ca0102"
95
+ },
96
+ {
97
+ "name": "extended-0-256",
98
+ "options": {
99
+ "cla": 0,
100
+ "ins": 202,
101
+ "p1": 1,
102
+ "p2": 2,
103
+ "data_hex": "",
104
+ "le": 256,
105
+ "extended": true
106
+ },
107
+ "expected_hex": "00ca0102000100"
108
+ },
109
+ {
110
+ "name": "extended-1-0",
111
+ "options": {
112
+ "cla": 0,
113
+ "ins": 202,
114
+ "p1": 1,
115
+ "p2": 2,
116
+ "data_hex": "00",
117
+ "le": null,
118
+ "extended": true
119
+ },
120
+ "expected_hex": "00ca010200000100"
121
+ },
122
+ {
123
+ "name": "extended-255-256",
124
+ "options": {
125
+ "cla": 0,
126
+ "ins": 202,
127
+ "p1": 1,
128
+ "p2": 2,
129
+ "data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
130
+ "le": 256,
131
+ "extended": true
132
+ },
133
+ "expected_hex": "00ca01020000ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe0100"
134
+ },
135
+ {
136
+ "name": "extended-256-0",
137
+ "options": {
138
+ "cla": 0,
139
+ "ins": 202,
140
+ "p1": 1,
141
+ "p2": 2,
142
+ "data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
143
+ "le": null,
144
+ "extended": true
145
+ },
146
+ "expected_hex": "00ca0102000100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
147
+ },
148
+ {
149
+ "name": "extended-512-1024",
150
+ "options": {
151
+ "cla": 0,
152
+ "ins": 202,
153
+ "p1": 1,
154
+ "p2": 2,
155
+ "data_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
156
+ "le": 1024,
157
+ "extended": true
158
+ },
159
+ "expected_hex": "00ca0102000200000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff0400"
160
+ }
161
+ ]
162
+ }
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tty-prompt'
5
+
6
+ RSpec.describe 'AuthenticationHelper native OATH integration' do
7
+ let(:account) { '-例:tester@example.invalid .+ ' }
8
+ let(:serial) { '00123456' }
9
+ let(:connection) { double('offline APDU connection', descriptor: { protocol: :apdu }, close: :closed) }
10
+ let(:yubikey) { { account: account, confirm: true, serial: serial, connection: connection, timeout: 5 }.freeze }
11
+ let(:select_apdu) { ['00a4040007a0000005272101'].pack('H*') }
12
+ let(:list_apdu) { ['00a1000000'].pack('H*') }
13
+ let(:calculate_apdu) do
14
+ body = [0x71, account.bytesize].pack('CC') + account.b + [0x74, 8].pack('CC') + [1].pack('Q>')
15
+ [0, 0xa2, 0, 1, body.bytesize].pack('C*') + body
16
+ end
17
+
18
+ before do
19
+ @ready = false
20
+ @frames = []
21
+ allow(Time).to receive(:now).and_return(Time.at(59))
22
+ allow(TTY::Prompt).to receive(:new).and_raise('unexpected interactive prompt')
23
+ expect(PWN::Plugins::YubiKey::Transport).not_to receive(:open)
24
+ expect(connection).not_to receive(:close)
25
+ allow(connection).to receive(:exchange) do |request|
26
+ expect(@ready).to be(true)
27
+ expect(request[:protocol]).to eq(:apdu)
28
+ @frames << request[:data]
29
+ case request[:data]
30
+ when select_apdu
31
+ [0x79, 3, 5, 7, 1, 0x71, 8].pack('C*') + 'fixture!'.b + "\x90\0".b
32
+ when list_apdu
33
+ [0x72, account.bytesize + 1, 0x21].pack('C*') + account.b + "\x90\0".b
34
+ when calculate_apdu
35
+ [0x76, 5, 8, 12_345].pack('CCCN') + "\x90\0".b
36
+ else
37
+ raise 'Unexpected APDU in offline OATH fixture'
38
+ end
39
+ end
40
+ end
41
+
42
+ it 'runs the real helper, root facade, OATH.code and Protocol against a fake APDU transport' do
43
+ @ready = true
44
+ options = { yubikey: yubikey }.freeze
45
+ expect do
46
+ expect(PWN::Plugins::AuthenticationHelper.mfa(options)).to eq('00012345')
47
+ end.not_to output.to_stdout
48
+ expect(@frames).to eq([select_apdu, list_apdu, calculate_apdu])
49
+ expect(options[:yubikey]).to equal(yubikey)
50
+ end
51
+
52
+ [nil, false, 'true'].each do |confirm|
53
+ it "rejects unconfirmed OATH #{confirm.inspect} before any APDU or prompt" do
54
+ expect do
55
+ PWN::Plugins::AuthenticationHelper.mfa(yubikey: yubikey.merge(confirm: confirm).freeze)
56
+ end.to raise_error(ArgumentError, /confirm: true/)
57
+ expect(@frames).to be_empty
58
+ end
59
+ end
60
+
61
+ it 'propagates exact-account errors without calculation, credential repair or manual fallback' do
62
+ @ready = true
63
+ expect do
64
+ PWN::Plugins::AuthenticationHelper.mfa(yubikey: yubikey.merge(account: 'missing@example.invalid'))
65
+ end.to raise_error(PWN::Plugins::YubiKey::ResponseError, /exact account/)
66
+ expect(@frames).to eq([select_apdu, list_apdu])
67
+ end
68
+
69
+ context 'through the real GitHub login and helper' do
70
+ let(:browser) { double('offline browser') }
71
+ let(:browser_obj) { { browser: browser } }
72
+ let(:credential_field) { double('offline credential field', set: nil, present?: true) }
73
+ let(:otp_field) { double('offline OATH field', present?: true, exist?: true) }
74
+ let(:submit) { double('offline submit', exist?: true) }
75
+ let(:login_opts) { { browser_obj: browser_obj, username: 'tester@example.invalid', password: 'fake-password', mfa: false, yubikey: yubikey } }
76
+
77
+ before do
78
+ @authenticated = false
79
+ @otp_written = false
80
+ allow(PWN::WWW::GitHub).to receive(:sleep)
81
+ allow(browser).to receive(:goto)
82
+ allow(browser).to receive(:url) { @authenticated ? 'https://github.com/' : 'https://github.com/sessions/two-factor' }
83
+ allow(browser).to receive(:text_field).and_return(credential_field)
84
+ allow(credential_field).to receive(:wait_until).and_return(credential_field)
85
+ allow(browser).to receive(:text_field).with(name: 'app_otp').and_return(otp_field)
86
+ allow(otp_field).to receive(:wait_until) do
87
+ expect(@frames).to be_empty
88
+ @ready = true
89
+ otp_field
90
+ end
91
+ allow(browser).to receive(:input).with(name: 'commit').and_return(submit)
92
+ allow(submit).to receive(:click!) { @authenticated = true if @otp_written }
93
+ end
94
+
95
+ it 'types actual native OATH digits only after the authenticator field is ready' do
96
+ expect(otp_field).to receive(:set).with('00012345') { @otp_written = true }
97
+ expect(PWN::WWW::GitHub.login(login_opts)).to equal(browser_obj)
98
+ expect(@frames).to eq([select_apdu, list_apdu, calculate_apdu])
99
+ end
100
+
101
+ it 'does not touch a token for an already authenticated redirect' do
102
+ @authenticated = true
103
+ expect(otp_field).not_to receive(:set)
104
+ expect(PWN::WWW::GitHub.login(login_opts)).to equal(browser_obj)
105
+ expect(@frames).to be_empty
106
+ end
107
+
108
+ it 'rejects absent consent before asking for a password or navigating' do
109
+ expect(PWN::Plugins::AuthenticationHelper).not_to receive(:mask_password)
110
+ expect(browser).not_to receive(:goto)
111
+ expect do
112
+ PWN::WWW::GitHub.login(login_opts.except(:password).merge(yubikey: yubikey.except(:confirm)))
113
+ end.to raise_error(ArgumentError, /confirm: true/)
114
+ expect(@frames).to be_empty
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'open3'
5
+ require 'tmpdir'
6
+
7
+ RSpec.describe 'build gate diagnostics' do
8
+ it 'reports the RSpec child signal rather than only a generic command failure' do
9
+ Dir.mktmpdir('pwn-rake-signal-') do |home|
10
+ spec = File.join(home, 'terminated_spec.rb')
11
+ File.write(spec, "RSpec.describe('fixture') { it('terminates') { Process.kill('KILL', Process.pid) } }\n")
12
+ output, status = Open3.capture2e({ 'HOME' => home, 'SPEC' => spec, 'SPEC_OPTS' => nil }, 'bundle', 'exec', 'rake', 'spec')
13
+ expect(status.success?).to be(false)
14
+ expect(output).to match(/RSpec child failed: (signal=KILL|exit=137)/)
15
+ end
16
+ end
17
+
18
+ it 'retains the first failure details even if a later example terminates RSpec' do
19
+ Dir.mktmpdir('pwn-rake-failure-') do |home|
20
+ spec = File.join(home, 'terminated_spec.rb')
21
+ File.write(spec, "RSpec.describe('fixture') { it('fails first') { expect(1).to eq(2) }; it('terminates') { Process.kill('KILL', Process.pid) } }\n")
22
+ output, status = Open3.capture2e({ 'HOME' => home, 'SPEC' => spec, 'SPEC_OPTS' => nil }, 'bundle', 'exec', 'rake', 'spec')
23
+ expect(status.success?).to be(false)
24
+ expect(output).to include('fixture fails first', 'expected: 2', 'got: 1')
25
+ end
26
+ end
27
+
28
+ it 'keeps a bounded log and preserves successful exit after large output' do
29
+ Dir.mktmpdir('pwn-build-bound-') do |home|
30
+ script = File.expand_path('../../scripts/build_gate.rb', __dir__)
31
+ output, status = Open3.capture2e({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', 'puts "x" * 2_000_000; puts "finished fixture"')
32
+ expect(status.success?).to be(true)
33
+ expect(output).to include('finished fixture')
34
+ log = File.join(home, '.pwn', 'logs', 'build-gate.log')
35
+ expect(File.size(log)).to be <= 1_048_576
36
+ expect(File.read(log)).to include('finished fixture', 'exit=0')
37
+ end
38
+ end
39
+
40
+ it 'records a directly signalled gate without inventing a summary' do
41
+ Dir.mktmpdir('pwn-build-signal-') do |home|
42
+ script = File.expand_path('../../scripts/build_gate.rb', __dir__)
43
+ output, status = Open3.capture2e({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', 'Process.kill("TERM", Process.pid)')
44
+ expect(status.exitstatus).to eq(143)
45
+ expect(output).to include('signal=TERM')
46
+ expect(File.read(File.join(home, '.pwn', 'logs', 'build-gate.log'))).to include('signal=TERM')
47
+ end
48
+ end
49
+
50
+ it 'finishes recording when both terminal streams disconnect' do
51
+ Dir.mktmpdir('pwn-build-disconnect-') do |home|
52
+ script = File.expand_path('../../scripts/build_gate.rb', __dir__)
53
+ reader, writer = IO.pipe
54
+ reader.close
55
+ pid = Process.spawn({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', 'warn "Failures: disconnected fixture"; exit 23', out: writer, err: writer)
56
+ writer.close
57
+ expect(Process.wait2(pid).last.exitstatus).to eq(23)
58
+ expect(File.read(File.join(home, '.pwn', 'logs', 'build-gate.log'))).to include('Failures: disconnected fixture', 'exit=23')
59
+ end
60
+ end
61
+
62
+ it 'preserves merged failure output and the gate exit status outside the terminal stream' do
63
+ Dir.mktmpdir('pwn-build-gate-') do |home|
64
+ script = File.expand_path('../../scripts/build_gate.rb', __dir__)
65
+ output, status = Open3.capture2e({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', '$stdout.sync = true; puts "F.F"; warn "Failures: fixture assertion"; exit 23')
66
+ expect(status.exitstatus).to eq(23)
67
+ expect(output).to include('Failures: fixture assertion', 'exit=23', 'Build gate log:')
68
+ log = File.join(home, '.pwn', 'logs', 'build-gate.log')
69
+ expect(File.read(log)).to include('F.F', 'Failures: fixture assertion', 'exit=23')
70
+ expect(File.stat(log).mode & 0o777).to eq(0o600)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'open3'
5
+ require 'tmpdir'
6
+ require 'fileutils'
7
+ require 'json'
8
+
9
+ RSpec.describe 'controller spill boundary through actual Dispatch' do
10
+ %w[checkpoint transcript].each do |mode|
11
+ it "does not create source files when a worker redirects the parent #{mode} spill path" do
12
+ Dir.mktmpdir('spill-boundary-') do |root|
13
+ source = File.join(root, 'source')
14
+ home = File.join(root, 'home')
15
+ work = File.join(root, 'work')
16
+ FileUtils.mkdir_p([source, home, work])
17
+ fixture = File.expand_path('../support/harness/controller_spill.rb', __dir__)
18
+ stdout, stderr, status = Open3.capture3({ 'HOME' => home }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, source, work, mode, chdir: work)
19
+ expect(status.success?).to be(true), stderr
20
+ row = JSON.parse(stdout)
21
+ expect(row['changed']).to be(false), row.inspect
22
+ expect(row['dispatch_success']).to be(false)
23
+ expect(row['dispatch_error']).to include('Permission denied')
24
+ end
25
+ end
26
+ end
27
+
28
+ %w[ordinary aliased].each do |mode|
29
+ it "denies the #{mode} controller-state inventory while preserving workspace output" do
30
+ Dir.mktmpdir('controller-inventory-') do |root|
31
+ source = File.join(root, 'source')
32
+ home = File.join(root, 'home')
33
+ work = File.join(root, 'work')
34
+ FileUtils.mkdir_p([source, home, work])
35
+ fixture = File.expand_path('../support/harness/controller_state.rb', __dir__)
36
+ stdout, stderr, status = Open3.capture3({ 'HOME' => home }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, source, work, mode, chdir: work)
37
+ expect(status.success?).to be(true), stderr
38
+ row = JSON.parse(stdout)
39
+ expect(row.dig('result', 'success')).to be(true), row.inspect
40
+ attempts = row.dig('result', 'result')
41
+ expect(attempts.length).to eq(29)
42
+ expect(attempts.values.uniq).to eq(['denied']), attempts.inspect
43
+ expect(row['source_unchanged']).to be(true)
44
+ expect(row['output']).to eq('output remains writable')
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require_relative '../support/harness/package_acceptance'
5
+ require_relative '../support/harness/cli_faults'
6
+
7
+ RSpec.describe 'harness process lifecycle' do
8
+ it 'preserves actual exit and stderr when startup fails before readiness' do
9
+ Dir.mktmpdir('pwn-harness-process-') do |root|
10
+ ready = File.join(root, 'ready')
11
+ command = [RbConfig.ruby, '-e', "warn 'unsupported architecture fixture'; exit 78"]
12
+ result = HarnessEvaluation.capture({ 'HOME' => root }, command, root, 1, ready_path: ready, startup_seconds: 2)
13
+ expect(result).to include(exit_status: 78, stderr: "unsupported architecture fixture\n", resource_cap: false, ready_observed: false, timeout_phase: nil)
14
+ end
15
+ end
16
+
17
+ it 'distinguishes a bounded boot timeout from an execution timeout' do
18
+ Dir.mktmpdir('pwn-harness-process-') do |root|
19
+ result = HarnessEvaluation.capture({ 'HOME' => root }, [RbConfig.ruby, '-e', 'sleep 60'], root, 1,
20
+ ready_path: File.join(root, 'ready'), startup_seconds: 0.05)
21
+ expect(result).to include(resource_cap: true, ready_observed: false, timeout_phase: 'startup', exit_status: nil, signal: Signal.list.fetch('KILL'))
22
+ end
23
+ end
24
+
25
+ it 'reports the real child failure when its fingerprint is missing, without inventing one' do
26
+ Dir.mktmpdir('pwn-harness-process-') do |root|
27
+ process = HarnessEvaluation.capture({ 'HOME' => root }, [RbConfig.ruby, '-e', "warn 'namespace unavailable'; exit 78"], root, 2)
28
+ File.write(File.join(root, 'initial-process.json'), JSON.generate(process))
29
+ row = { id: 'checkpoint-restart', artifact_root: root }
30
+ expect { HarnessPackageAcceptance.validate_fingerprints!([row], '/isolated/gems/pwn') }
31
+ .to raise_error(RuntimeError) { |error| expect(error.message).to include('checkpoint-restart', 'fingerprint', '"exit_status":78', 'namespace unavailable') }
32
+ expect(File.exist?(File.join(root, 'fingerprint.json'))).to be(false)
33
+ end
34
+ end
35
+
36
+ it 'records observed loaded components before an abrupt fixture exit bypasses at_exit' do
37
+ Dir.mktmpdir('pwn-harness-process-') do |root|
38
+ fixture = File.join(root, 'provider.json')
39
+ fingerprint = File.join(root, 'fingerprint.json')
40
+ File.write(fixture, JSON.generate(responses: []))
41
+ env = { 'HOME' => root, 'GEM_HOME' => Gem.dir, 'GEM_PATH' => Gem.path.join(File::PATH_SEPARATOR),
42
+ 'PWN_HARNESS_FIXTURE' => fixture, 'PWN_HARNESS_EVENTS' => File.join(root, 'events.jsonl'), 'PWN_HARNESS_FINGERPRINT' => fingerprint }
43
+ command = [RbConfig.ruby, '-I', File.join(HarnessEvaluation::ROOT, 'lib'), '-r',
44
+ File.join(HarnessEvaluation::ROOT, 'spec/support/harness/fake_provider.rb'), '-e', 'exit!(73)']
45
+ result = HarnessEvaluation.agent_capture(env, command, root, 5)
46
+ expect(result).to include(exit_status: 73, ready_observed: true)
47
+ expect(File.file?(fingerprint)).to be(true), result.inspect
48
+ expect(JSON.parse(File.read(fingerprint)).fetch('components')).not_to be_empty
49
+ end
50
+ end
51
+
52
+ it 'loads checkpoint controls across the real namespace exec boundary' do
53
+ Dir.mktmpdir('pwn-harness-process-') do |root|
54
+ fixture = File.join(root, 'provider.json')
55
+ runtime = File.join(root, 'runtime')
56
+ File.write(fixture, JSON.generate(responses: [{ fault: 'crash' }]))
57
+ env = { 'HOME' => root, 'PATH' => HarnessEvaluation.isolated_path, 'GEM_HOME' => Gem.dir, 'GEM_PATH' => Gem.path.join(File::PATH_SEPARATOR),
58
+ 'PWN_HARNESS_FIXTURE' => fixture, 'PWN_HARNESS_EVENTS' => File.join(root, 'events.jsonl'),
59
+ 'PWN_HARNESS_FINGERPRINT' => File.join(root, 'fingerprint.json'), 'PWN_HARNESS_RUNTIME' => runtime }
60
+ command = [RbConfig.ruby, '-I', File.join(HarnessEvaluation::ROOT, 'lib'), '-r',
61
+ File.join(HarnessEvaluation::ROOT, 'spec/support/harness/fault_preload.rb'), '-e',
62
+ 'PWN::AI::Agent::MissionNamespace.force_exec = true; load ARGV.shift', File.join(HarnessEvaluation::ROOT, 'bin/pwn-ai'), '--ai', 'Offline checkpoint fixture']
63
+ result = HarnessEvaluation.agent_capture(env, command, root, 10)
64
+ expect(result).to include(exit_status: 73)
65
+ expect(File.file?(File.join(runtime, 'identity.json'))).to be(true), result.inspect
66
+ end
67
+ end
68
+
69
+ it 'does not resume a checkpoint that never started after a real child failure' do
70
+ Dir.mktmpdir('pwn-harness-process-') do |root|
71
+ failure = { exit_status: 78, resource_cap: false, ready_observed: false, stdout: '', stderr: 'namespace unavailable' }
72
+ allow(HarnessCLIFaults).to receive(:invoke).and_return(failure)
73
+ report = HarnessCLIFaults.run(output: root)
74
+ expect(HarnessCLIFaults).to have_received(:invoke).exactly(5).times
75
+ expect(report.fetch(:results)).to all(include(outcome: 'FAIL', exit_status: 78))
76
+ end
77
+ end
78
+
79
+ it 'redacts child diagnostics before reporting a missing fingerprint' do
80
+ Dir.mktmpdir('pwn-harness-process-') do |root|
81
+ row = { id: 'failed-boot', artifact_root: root, exit_status: 78, stderr: 'password=offline-secret' }
82
+ expect { HarnessPackageAcceptance.validate_fingerprints!([row], '/isolated/gems/pwn') }.to raise_error(RuntimeError) do |error|
83
+ expect(error.message).to include('REDACTED', '78')
84
+ expect(error.message).not_to include('offline-secret')
85
+ end
86
+ end
87
+ end
88
+
89
+ it 'requires fresh readiness on restart rather than accepting the previous process marker' do
90
+ Dir.mktmpdir('pwn-harness-process-') do |root|
91
+ File.write(File.join(root, 'harness-ready'), 'previous invocation')
92
+ command = [RbConfig.ruby, '-e', "sleep 0.2; puts 'booted'; STDOUT.flush; File.write(ENV.fetch('PWN_HARNESS_READY'), 'ready'); sleep 60"]
93
+ result = HarnessEvaluation.agent_capture({ 'HOME' => root }, command, root, 0.05)
94
+ expect(result).to include(resource_cap: true, ready_observed: true, stdout: "booted\n", timeout_phase: 'execution')
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'open3'
5
+ require 'tmpdir'
6
+ require 'fileutils'
7
+ require 'json'
8
+
9
+ RSpec.describe 'mission cancellation through real subprocesses' do
10
+ def mission_fixture(name, *args)
11
+ Dir.mktmpdir('mission-cancel-') do |root|
12
+ home = File.join(root, 'home')
13
+ work = File.join(root, 'work')
14
+ FileUtils.mkdir_p([home, work])
15
+ fixture = File.expand_path("../support/harness/#{name}.rb", __dir__)
16
+ stdout, stderr, status = Open3.capture3({ 'HOME' => home }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, work, *args, chdir: work)
17
+ expect(status.success?).to be(true), stderr
18
+ JSON.parse(stdout)
19
+ end
20
+ end
21
+
22
+ it 'cancels the legacy interactive entrypoint through a native terminal and accepts its next mission' do
23
+ require 'pty'
24
+ require 'timeout'
25
+ Dir.mktmpdir('mission-legacy-') do |root|
26
+ FileUtils.mkdir_p(File.join(root, 'home'))
27
+ fixture = File.expand_path('../support/harness/mission_legacy.rb', __dir__)
28
+ PTY.spawn({ 'HOME' => File.join(root, 'home') }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, root, chdir: root) do |reader, writer, pid|
29
+ Timeout.timeout(15) { sleep 0.01 until File.exist?(File.join(root, 'ready')) }
30
+ writer.write("\x03")
31
+ output = +''
32
+ Timeout.timeout(15) do
33
+ loop { output << reader.readpartial(4096) }
34
+ rescue EOFError, Errno::EIO
35
+ nil
36
+ end
37
+ _child, status = Process.wait2(pid)
38
+ expect(status.success?).to be(true), output
39
+ row = output.lines.find { |line| line.start_with?('{') }
40
+ expect(JSON.parse(row)).to include('ready' => true, 'late' => false, 'second' => true, 'tty' => true)
41
+ ensure
42
+ begin
43
+ Process.kill('KILL', pid)
44
+ Process.wait(pid)
45
+ rescue Errno::ESRCH, Errno::ECHILD
46
+ nil
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ it 'reaps provider execution before accepting the next console mission' do
53
+ expect(mission_fixture('mission_console')).to include('ready' => true, 'late' => false, 'second' => true, 'fenced' => true)
54
+ end
55
+
56
+ it 'hard cancels local command descendants before the next mission' do
57
+ expect(mission_fixture('mission_console', 'command')).to include('ready' => true, 'late' => false, 'second' => true, 'fenced' => true)
58
+ end
59
+
60
+ it 'keeps detached tool jobs alive until their enclosing mission exits' do
61
+ expect(mission_fixture('mission_job_lifetime')).to include('survived_tool' => true)
62
+ end
63
+
64
+ it 'hard cancels independent swarm provider and tool descendants' do
65
+ expect(mission_fixture('mission_swarm')).to include('ready' => true, 'late' => false)
66
+ end
67
+
68
+ it 'fails closed and reaps startup when namespace creation is denied' do
69
+ expect(mission_fixture('mission_unsupported')).to include('blocked' => true, 'actionable' => true, 'reaped' => true)
70
+ end
71
+
72
+ it 'owns double-forked jobs on normal exit without killing unrelated processes' do
73
+ expect(mission_fixture('mission_normal_exit')).to include('reaped' => true, 'late' => false, 'unrelated' => true)
74
+ end
75
+
76
+ it 'stops and reaps a setsid descendant before cancellation returns' do
77
+ expect(mission_fixture('mission_dispatch')).to include('cancelled' => true, 'descendant_started' => true, 'reaped' => true, 'post_cancel_write' => false)
78
+ end
79
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'pty'
5
+ require 'io/console'
6
+ require 'timeout'
7
+
8
+ RSpec.describe 'native curses mission cancellation' do
9
+ %w[ordinary command].each do |mode|
10
+ it "handles terminal CTRL+C during #{mode} execution and accepts the next mission without late writes" do
11
+ Dir.mktmpdir('mission-curses-') do |root|
12
+ home = File.join(root, 'home')
13
+ work = File.join(root, 'work')
14
+ FileUtils.mkdir_p([home, work])
15
+ fixture = File.expand_path('../support/harness/mission_curses_pty.rb', __dir__)
16
+ PTY.spawn({ 'HOME' => home, 'TERM' => 'xterm-256color' }, RbConfig.ruby, '-I', File.expand_path('../../lib', __dir__), fixture, work, mode, chdir: work) do |reader, writer, pid|
17
+ writer.winsize = [32, 120]
18
+ output = +''
19
+ pump = Thread.new do
20
+ loop { output << reader.readpartial(4096) }
21
+ rescue EOFError, Errno::EIO
22
+ nil
23
+ end
24
+ %w[idle ready cancelled second].zip([mode == 'command' ? "/mcp fixture\r" : "first\r", "\x03", "second\r", "back\r"]).each do |marker, keys|
25
+ Timeout.timeout(20) { sleep 0.01 until File.exist?(File.join(work, marker)) }
26
+ writer.write(keys)
27
+ end
28
+ Timeout.timeout(20) { pump.join }
29
+ _child, status = Process.wait2(pid)
30
+ expect(status.success?).to be(true), output
31
+ expect(JSON.parse(output[/\{"ready":.*\}/])).to include('ready' => true, 'late' => false, 'second' => true, 'tty' => true)
32
+ ensure
33
+ begin
34
+ Process.kill('KILL', pid)
35
+ Process.wait(pid)
36
+ rescue Errno::ESRCH, Errno::ECHILD
37
+ nil
38
+ end
39
+ pump&.join(1)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -58,13 +58,7 @@ RSpec.describe 'PWN persistence stores — write → reload → delete round-tri
58
58
  expect(PWN::Cron.list.keys).not_to include(job[:id])
59
59
 
60
60
  seeded = PWN::Cron.install_defaults
61
- expect(seeded.map { |j| j[:name] }).to include(
62
- 'curriculum_practice_nightly',
63
- 'curriculum_train_weekly',
64
- 'curriculum_offline_judge',
65
- 'learning_consolidate_nightly',
66
- 'pwn_stores_lean_nightly'
67
- )
61
+ expect(seeded.map { |j| j[:name] }).to eq(['session_cleanup'])
68
62
  expect(PWN::Cron.install_defaults).to eq([]) # idempotent
69
63
  end
70
64
  end