pwn 0.5.615 → 0.5.618

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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +109 -85
  3. data/README.md.bak +200 -0
  4. data/bin/pwn +47 -2
  5. data/bin/pwn_gqrx_scanner +75 -18
  6. data/documentation/AI-Integration.md +42 -26
  7. data/documentation/AWS.md +57 -0
  8. data/documentation/Agent-Tool-Registry.md +56 -0
  9. data/documentation/Banner.md +17 -0
  10. data/documentation/Blockchain.md +18 -0
  11. data/documentation/Bounty.md +21 -0
  12. data/documentation/BurpSuite.md +41 -16
  13. data/documentation/CLI-Drivers.md +58 -0
  14. data/documentation/Configuration.md +66 -0
  15. data/documentation/Contributing.md +33 -19
  16. data/documentation/Cron.md +47 -0
  17. data/documentation/Diagrams.md +123 -40
  18. data/documentation/Drivers.md +43 -16
  19. data/documentation/Extrospection.md +54 -0
  20. data/documentation/FFI.md +14 -0
  21. data/documentation/Fuzzing.md +36 -0
  22. data/documentation/General-PWN-Usage.md +50 -30
  23. data/documentation/Hardware.md +40 -0
  24. data/documentation/Home.md +78 -51
  25. data/documentation/How-PWN-Works.md +59 -57
  26. data/documentation/Installation.md +53 -28
  27. data/documentation/Metasploit.md +34 -0
  28. data/documentation/NmapIt.md +18 -12
  29. data/documentation/PWN.png +0 -0
  30. data/documentation/PWN_Contributors_and_Users.png +0 -0
  31. data/documentation/Persistence.md +38 -0
  32. data/documentation/Plugins.md +97 -47
  33. data/documentation/Reporting.md +25 -18
  34. data/documentation/SAST.md +39 -22
  35. data/documentation/SDR.md +40 -0
  36. data/documentation/Sessions.md +39 -0
  37. data/documentation/Skills-Memory-Learning.md +49 -29
  38. data/documentation/Swarm.md +71 -0
  39. data/documentation/Transparent-Browser.md +26 -22
  40. data/documentation/Troubleshooting.md +44 -25
  41. data/documentation/WWW.md +32 -0
  42. data/documentation/What-is-PWN.md +51 -32
  43. data/documentation/Why-PWN.md +41 -11
  44. data/documentation/diagrams/agent-tool-registry.svg +284 -0
  45. data/documentation/diagrams/ai-integration-tool-calling.svg +141 -103
  46. data/documentation/diagrams/aws-cloud-security.svg +166 -0
  47. data/documentation/diagrams/build.sh +20 -0
  48. data/documentation/diagrams/burp-vs-zap-preference.svg +47 -62
  49. data/documentation/diagrams/code-scanning-sast.svg +139 -79
  50. data/documentation/diagrams/cron-scheduling.svg +148 -0
  51. data/documentation/diagrams/dot/_THEME.md +27 -0
  52. data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
  53. data/documentation/diagrams/dot/ai-integration-tool-calling.dot +47 -20
  54. data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
  55. data/documentation/diagrams/dot/burp-vs-zap-preference.dot +23 -12
  56. data/documentation/diagrams/dot/code-scanning-sast.dot +46 -18
  57. data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
  58. data/documentation/diagrams/dot/driver-framework.dot +36 -14
  59. data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
  60. data/documentation/diagrams/dot/fuzzing-workflow.dot +44 -24
  61. data/documentation/diagrams/dot/hardware-hacking.dot +48 -0
  62. data/documentation/diagrams/dot/history-to-drivers.dot +31 -17
  63. data/documentation/diagrams/dot/memory-skills-detailed.dot +57 -25
  64. data/documentation/diagrams/dot/network-infra-testing.dot +51 -19
  65. data/documentation/diagrams/dot/overall-pwn-architecture.dot +110 -44
  66. data/documentation/diagrams/dot/penetration-testing-workflow.dot +56 -25
  67. data/documentation/diagrams/dot/persistence-filesystem.dot +29 -0
  68. data/documentation/diagrams/dot/plugin-ecosystem.dot +41 -27
  69. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +87 -46
  70. data/documentation/diagrams/dot/pwn-repl-prototyping.dot +49 -30
  71. data/documentation/diagrams/dot/reporting-pipeline.dot +43 -14
  72. data/documentation/diagrams/dot/reverse-engineering-flow.dot +40 -18
  73. data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
  74. data/documentation/diagrams/dot/sessions-cron-automation.dot +41 -14
  75. data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
  76. data/documentation/diagrams/dot/web-application-testing.dot +45 -23
  77. data/documentation/diagrams/dot/zero-day-research-flow.dot +46 -24
  78. data/documentation/diagrams/driver-framework.svg +87 -66
  79. data/documentation/diagrams/extrospection-world-awareness.svg +185 -0
  80. data/documentation/diagrams/fuzzing-workflow.svg +128 -115
  81. data/documentation/diagrams/hardware-hacking.svg +163 -0
  82. data/documentation/diagrams/history-to-drivers.svg +84 -82
  83. data/documentation/diagrams/memory-skills-detailed.svg +167 -119
  84. data/documentation/diagrams/network-infra-testing.svg +146 -84
  85. data/documentation/diagrams/overall-pwn-architecture.svg +356 -185
  86. data/documentation/diagrams/penetration-testing-workflow.svg +161 -108
  87. data/documentation/diagrams/persistence-filesystem.svg +174 -0
  88. data/documentation/diagrams/plugin-ecosystem.svg +225 -121
  89. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +257 -185
  90. data/documentation/diagrams/pwn-repl-prototyping.svg +126 -137
  91. data/documentation/diagrams/reporting-pipeline.svg +128 -68
  92. data/documentation/diagrams/reverse-engineering-flow.svg +100 -80
  93. data/documentation/diagrams/sdr-radio-flow.svg +146 -0
  94. data/documentation/diagrams/sessions-cron-automation.svg +112 -68
  95. data/documentation/diagrams/swarm-multi-agent.svg +225 -0
  96. data/documentation/diagrams/web-application-testing.svg +136 -100
  97. data/documentation/diagrams/zero-day-research-flow.svg +116 -112
  98. data/documentation/fax-spectrogram.png +0 -0
  99. data/documentation/fax-waveform.png +0 -0
  100. data/documentation/pwn-REPL.md +40 -24
  101. data/documentation/pwn-ai-Agent.md +59 -30
  102. data/documentation/pwn_android_war_dialer_session.png +0 -0
  103. data/documentation/pwn_install.png +0 -0
  104. data/documentation/pwn_wallpaper.jpg +0 -0
  105. data/documentation/ringing-spectrogram.png +0 -0
  106. data/documentation/ringing-waveform.png +0 -0
  107. data/git_commit.sh +1 -1
  108. data/lib/pwn/ai/agent/assembly.rb +1 -1
  109. data/lib/pwn/ai/agent/btc.rb +1 -1
  110. data/lib/pwn/ai/agent/burp_suite.rb +1 -1
  111. data/lib/pwn/ai/agent/extrospection.rb +618 -0
  112. data/lib/pwn/ai/agent/gqrx.rb +2 -2
  113. data/lib/pwn/ai/agent/hacker_one.rb +1 -1
  114. data/lib/pwn/ai/agent/introspection.rb +91 -0
  115. data/lib/pwn/ai/agent/learning.rb +6 -4
  116. data/lib/pwn/ai/agent/loop.rb +15 -0
  117. data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
  118. data/lib/pwn/ai/agent/sast.rb +1 -1
  119. data/lib/pwn/ai/agent/swarm.rb +437 -0
  120. data/lib/pwn/ai/agent/tools/cron.rb +163 -0
  121. data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
  122. data/lib/pwn/ai/agent/tools/learning.rb +108 -0
  123. data/lib/pwn/ai/agent/tools/memory.rb +27 -0
  124. data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
  125. data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
  126. data/lib/pwn/ai/agent/tools/skills.rb +30 -0
  127. data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
  128. data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
  129. data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
  130. data/lib/pwn/ai/agent.rb +3 -0
  131. data/lib/pwn/ai/anthropic.rb +19 -4
  132. data/lib/pwn/ai.rb +0 -1
  133. data/lib/pwn/config.rb +10 -23
  134. data/lib/pwn/cron.rb +16 -7
  135. data/lib/pwn/plugins/repl.rb +90 -281
  136. data/lib/pwn/sdr/decoder/base.rb +251 -0
  137. data/lib/pwn/sdr/decoder/gsm.rb +84 -185
  138. data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
  139. data/lib/pwn/sdr/decoder.rb +1 -0
  140. data/lib/pwn/sdr/gqrx.rb +446 -65
  141. data/lib/pwn/version.rb +1 -1
  142. data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
  143. data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
  144. data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
  145. data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
  146. data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
  147. data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
  148. data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
  149. data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
  150. data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
  151. data/third_party/pwn_rdoc.jsonl +199 -37
  152. metadata +65 -3
  153. data/lib/pwn/ai/introspection.rb +0 -76
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module PWN
6
+ module AI
7
+ module Agent
8
+ # PWN::AI::Agent::Introspection is the inward-facing counterpart to
9
+ # PWN::AI::Agent::Extrospection. Where Extrospection looks OUTWARD at
10
+ # the world the agent operates in (host state, toolchain, network,
11
+ # threat-intel), Introspection looks INWARD - it lets pwn hand a
12
+ # request to the active AI engine and reflect on its own artifacts,
13
+ # transcripts, findings, code, or decisions.
14
+ #
15
+ # This module is gated by `PWN::Env[:ai][:introspection]` so that
16
+ # potentially-sensitive local data is never shipped to a remote LLM
17
+ # unless the operator has explicitly opted in via pwn-vault / config.
18
+ #
19
+ # It is the single choke-point every PWN::AI::Agent::* domain agent
20
+ # (Assembly, BurpSuite, GQRX, HackerOne, SAST, VulnGen, ...) routes
21
+ # through when it wants an LLM opinion on locally-produced data, and
22
+ # it is also what PWN::AI::Agent::Learning.reflect uses to distill
23
+ # session transcripts into durable PWN::Memory lessons.
24
+ module Introspection
25
+ # Supported Method Parameters::
26
+ # response = PWN::AI::Agent::Introspection.reflect_on(
27
+ # request: 'required - String - What you want the AI to reflect on',
28
+ # system_role_content: 'optional - context to set up the model behavior for reflection',
29
+ # spinner: 'optional - Boolean - Display spinner during operation (default: false)',
30
+ # suppress_pii_warning: 'optional - Boolean - Suppress PII Warnings (default: false)'
31
+ # )
32
+
33
+ public_class_method def self.reflect_on(opts = {})
34
+ request = opts[:request]
35
+ raise 'ERROR: request must be provided' if request.nil?
36
+
37
+ system_role_content = opts[:system_role_content]
38
+
39
+ spinner = opts[:spinner] || false
40
+
41
+ suppress_pii_warning = opts[:suppress_pii_warning] || false
42
+
43
+ response = nil
44
+
45
+ ai_introspection = PWN::Env[:ai][:introspection]
46
+
47
+ if ai_introspection && request.length.positive?
48
+ engine = PWN::Env[:ai][:active].to_s.downcase.to_sym
49
+ valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :agent }.map(&:downcase)
50
+ raise "ERROR: Unsupported AI engine. Supported engines are: #{valid_ai_engines}" unless valid_ai_engines.include?(engine)
51
+
52
+ warn "AI Introspection is enabled. Ensure #{engine} has been authorized for use and/or requests are sanitized properly." unless suppress_pii_warning
53
+ response = PWN::AI::Agent::Loop.run(
54
+ request: request.chomp,
55
+ system_role_content: system_role_content,
56
+ enabled_toolsets: [],
57
+ spinner: spinner
58
+ )
59
+ end
60
+
61
+ response
62
+ rescue StandardError => e
63
+ raise e
64
+ end
65
+
66
+ # Author(s):: 0day Inc. <support@0dayinc.com>
67
+
68
+ public_class_method def self.authors
69
+ "AUTHOR(S):
70
+ 0day Inc. <support@0dayinc.com>
71
+ "
72
+ end
73
+
74
+ # Display Usage for this Module
75
+
76
+ public_class_method def self.help
77
+ puts "USAGE:
78
+ #{self}.reflect_on(
79
+ request: 'required - String - What you want the AI to reflect on',
80
+ system_role_content: 'optional - context to set up the model behavior for reflection',
81
+ spinner: 'optional - Boolean - Display spinner during operation (default: false)',
82
+ suppress_pii_warning: 'optional - Boolean - Suppress PII Warnings (default: false)'
83
+ )
84
+
85
+ #{self}.authors
86
+ "
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -100,7 +100,8 @@ module PWN
100
100
  success_rate: total.positive? ? (ok.to_f / total).round(3) : 0.0,
101
101
  skills_known: skills,
102
102
  memory_entries: mem,
103
- tool_metrics: (Metrics.summary(limit: 5) if defined?(Metrics))
103
+ tool_metrics: (Metrics.summary(limit: 5) if defined?(Metrics)),
104
+ extrospection: (Extrospection.stats if defined?(Extrospection))
104
105
  }
105
106
  end
106
107
 
@@ -160,7 +161,7 @@ module PWN
160
161
  # dry_run: 'optional - when true, do not write to Memory/Skills (default false)'
161
162
  # )
162
163
  #
163
- # Uses PWN::AI::Introspection (when available) to LLM-summarise the
164
+ # Uses PWN::AI::Agent::Introspection (when available) to LLM-summarise the
164
165
  # session into structured lessons. Falls back to a heuristic
165
166
  # extractor when introspection is disabled so learning never stops.
166
167
 
@@ -211,6 +212,7 @@ module PWN
211
212
  tags: %w[auto loop]
212
213
  )
213
214
  reflect(session_id: session_id)
215
+ Extrospection.auto_extrospect(session_id: session_id) if defined?(Extrospection)
214
216
  rescue StandardError => e
215
217
  warn "[pwn-ai/learning] auto_reflect swallowed: #{e.class}: #{e.message}"
216
218
  nil
@@ -291,11 +293,11 @@ module PWN
291
293
 
292
294
  private_class_method def self.introspective_lessons(opts = {})
293
295
  transcript = opts[:transcript] || []
294
- return [] unless defined?(PWN::AI::Introspection)
296
+ return [] unless defined?(PWN::AI::Agent::Introspection)
295
297
  return [] unless defined?(PWN::Env) && PWN::Env.is_a?(Hash) && PWN::Env.dig(:ai, :introspection)
296
298
 
297
299
  req = "Analyse this pwn-ai session transcript and emit up to 5 durable, generalizable lessons (one per line, no numbering, imperative voice) that would make future runs faster or more reliable. Focus on tool selection, error recovery, and target-agnostic technique. Ignore trivia.\n\nTRANSCRIPT:\n#{transcript_text(transcript: transcript)}"
298
- resp = PWN::AI::Introspection.reflect_on(request: req, suppress_pii_warning: true)
300
+ resp = PWN::AI::Agent::Introspection.reflect_on(request: req, suppress_pii_warning: true)
299
301
  resp.to_s.lines.map(&:strip).reject(&:empty?).first(5)
300
302
  rescue StandardError
301
303
  []
@@ -61,6 +61,20 @@ module PWN
61
61
  nil
62
62
  end
63
63
 
64
+ # Stash the active session_id under PWN::Env[:ai][:session_id] so
65
+ # tool handlers (sessions_current) can discover it without a Pry
66
+ # dependency. PWN::Env is frozen at the top level but [:ai] is a
67
+ # nested mutable Hash on all supported config paths — swallow if not.
68
+ private_class_method def self.expose_current_session(opts = {})
69
+ sid = opts[:session_id]
70
+ return unless sid && defined?(PWN::Env) && PWN::Env.is_a?(Hash)
71
+
72
+ ai = PWN::Env[:ai]
73
+ ai[:session_id] = sid if ai.is_a?(Hash) && !ai.frozen?
74
+ rescue StandardError
75
+ nil
76
+ end
77
+
64
78
  private_class_method def self.append_session(opts = {})
65
79
  session_id = opts[:session_id]
66
80
  return unless session_id && defined?(PWN::Sessions)
@@ -157,6 +171,7 @@ module PWN
157
171
  system_role_content = opts[:system_role_content] ||= PWN::AI::Agent::PromptBuilder.build(session_id: session_id)
158
172
 
159
173
  Registry.discover
174
+ expose_current_session(session_id: session_id)
160
175
 
161
176
  tools = Registry.definitions(enabled: opts[:enabled_toolsets])
162
177
  messages = [
@@ -31,7 +31,7 @@ module PWN
31
31
  pwn : #{pwn_version}
32
32
  session_id : #{session_id || '(none)'}
33
33
 
34
- #{memory_block}#{skills_block}#{learning_block}#{metrics_block}TOOL USE
34
+ #{memory_block}#{skills_block}#{learning_block}#{metrics_block}#{extrospection_block}TOOL USE
35
35
  Use the provided function tools to act on the host. A reply with
36
36
  no tool_calls is treated as your FINAL answer to the user.
37
37
  Prefer `pwn_eval` for anything in the PWN:: namespace and `shell`
@@ -99,6 +99,15 @@ module PWN
99
99
  ''
100
100
  end
101
101
 
102
+ private_class_method def self.extrospection_block
103
+ return '' unless defined?(PWN::AI::Agent::Extrospection)
104
+
105
+ ctx = PWN::AI::Agent::Extrospection.to_context.to_s
106
+ ctx.strip.empty? ? '' : ctx
107
+ rescue StandardError
108
+ ''
109
+ end
110
+
102
111
  # Author(s):: 0day Inc. <support@0dayinc.com>
103
112
 
104
113
  public_class_method def self.authors
@@ -16,7 +16,7 @@ module PWN
16
16
 
17
17
  system_role_content = 'Your sole purpose is to analyze source code snippets and generate an Exploit Prediction Scoring System (EPSS) score between 0% - 100%. Just generate a score unless score is >= 75% in which a PoC and code fix should also be included.'
18
18
 
19
- PWN::AI::Introspection.reflect_on(
19
+ PWN::AI::Agent::Introspection.reflect_on(
20
20
  system_role_content: system_role_content,
21
21
  request: request,
22
22
  suppress_pii_warning: true
@@ -0,0 +1,437 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'yaml'
5
+ require 'time'
6
+ require 'fileutils'
7
+ require 'securerandom'
8
+
9
+ module PWN
10
+ module AI
11
+ module Agent
12
+ # Native multi-agent orchestration for pwn-ai.
13
+ #
14
+ # Swarm replaces the legacy `pwn-irc` mechanism (inspircd + weechat +
15
+ # PRIVMSG-flattened .chat calls) with first-class sub-agents built on
16
+ # top of PWN::AI::Agent::Loop.run. Each persona is a full tool-calling
17
+ # agent — Memory, Skills, Learning, Metrics and Extrospection all
18
+ # apply — so the self-improvement loop covers the whole swarm.
19
+ #
20
+ # ~/.pwn/agents.yml # persona registry
21
+ # ~/.pwn/swarm/<swarm_id>/bus.jsonl # append-only message bus
22
+ # ~/.pwn/swarm/<swarm_id>/personas.json# persona -> PWN::Sessions id
23
+ #
24
+ # Cross-session / cross-process communication == another pwn-ai (or a
25
+ # PWN::Cron job) calling Swarm.ask/debate with the same swarm_id and
26
+ # reading the same bus.jsonl. No daemon required.
27
+ module Swarm
28
+ AGENTS_FILE = File.join(Dir.home, '.pwn', 'agents.yml')
29
+ SWARM_ROOT = File.join(Dir.home, '.pwn', 'swarm')
30
+ DEFAULT_DEPTH = 3
31
+ DEFAULT_ITERS = 25
32
+ DEFAULT_TAIL = 12
33
+ DEFAULT_TOOLSET = %w[terminal pwn memory skills sessions learning
34
+ metrics extrospection].freeze
35
+
36
+ # ------------------------------------------------------------------
37
+ # Persona registry (~/.pwn/agents.yml)
38
+ # ------------------------------------------------------------------
39
+
40
+ # Supported Method Parameters::
41
+ # personas = PWN::AI::Agent::Swarm.personas
42
+
43
+ public_class_method def self.personas
44
+ return {} unless File.exist?(AGENTS_FILE)
45
+
46
+ raw = YAML.safe_load_file(
47
+ AGENTS_FILE,
48
+ permitted_classes: [Symbol],
49
+ aliases: true,
50
+ symbolize_names: true
51
+ ) || {}
52
+ raw.transform_values { |v| normalize_persona(persona: v) }
53
+ rescue StandardError => e
54
+ warn "[pwn-ai/swarm] failed to load #{AGENTS_FILE}: #{e.class}: #{e.message}"
55
+ {}
56
+ end
57
+
58
+ # Supported Method Parameters::
59
+ # PWN::AI::Agent::Swarm.spawn(
60
+ # name: 'required - persona name (snake_case)',
61
+ # role: 'required - system_role_content overlay for this persona',
62
+ # toolsets: 'optional - Array of Registry toolset names',
63
+ # engine: 'optional - :openai / :anthropic / :grok / :gemini / :ollama',
64
+ # max_iters: 'optional - per-turn iteration cap for this persona'
65
+ # )
66
+
67
+ public_class_method def self.spawn(opts = {})
68
+ name = opts[:name].to_s
69
+ raise ArgumentError, 'name is required' if name.strip.empty?
70
+ raise ArgumentError, 'role is required' if opts[:role].to_s.strip.empty?
71
+
72
+ all = personas
73
+ all[name.to_sym] = normalize_persona(persona: opts)
74
+ FileUtils.mkdir_p(File.dirname(AGENTS_FILE))
75
+ File.write(AGENTS_FILE, YAML.dump(deep_stringify(hash: all)))
76
+ { name: name, persona: all[name.to_sym], file: AGENTS_FILE }
77
+ end
78
+
79
+ # Supported Method Parameters::
80
+ # PWN::AI::Agent::Swarm.retire(name: 'required - persona name')
81
+
82
+ public_class_method def self.retire(opts = {})
83
+ name = opts[:name].to_s
84
+ all = personas
85
+ gone = all.delete(name.to_sym)
86
+ File.write(AGENTS_FILE, YAML.dump(deep_stringify(hash: all))) if gone
87
+ { name: name, removed: !gone.nil? }
88
+ end
89
+
90
+ # ------------------------------------------------------------------
91
+ # Swarm lifecycle & bus
92
+ # ------------------------------------------------------------------
93
+
94
+ # Supported Method Parameters::
95
+ # swarm = PWN::AI::Agent::Swarm.create(topic: 'optional')
96
+
97
+ public_class_method def self.create(opts = {})
98
+ id = "#{Time.now.utc.strftime('%Y%m%d_%H%M%S')}_#{SecureRandom.hex(3)}"
99
+ dir = File.join(SWARM_ROOT, id)
100
+ FileUtils.mkdir_p(dir)
101
+ bus_append(swarm_id: id, from: :system, to: :all,
102
+ content: "swarm #{id} created: #{opts[:topic] || '(no topic)'}")
103
+ { swarm_id: id, dir: dir, bus: bus_path(swarm_id: id) }
104
+ end
105
+
106
+ # Supported Method Parameters::
107
+ # swarms = PWN::AI::Agent::Swarm.list
108
+
109
+ public_class_method def self.list
110
+ FileUtils.mkdir_p(SWARM_ROOT)
111
+ Dir.children(SWARM_ROOT).sort.reverse.map do |id|
112
+ bp = bus_path(swarm_id: id)
113
+ {
114
+ swarm_id: id,
115
+ dir: File.join(SWARM_ROOT, id),
116
+ messages: File.exist?(bp) ? File.foreach(bp).count : 0,
117
+ mtime: File.exist?(bp) ? File.mtime(bp).utc.iso8601 : nil
118
+ }
119
+ end
120
+ end
121
+
122
+ # Supported Method Parameters::
123
+ # PWN::AI::Agent::Swarm.bus_append(
124
+ # swarm_id: 'required', from: 'required', content: 'required',
125
+ # to: 'optional (default :all)'
126
+ # )
127
+
128
+ public_class_method def self.bus_append(opts = {})
129
+ sid = opts[:swarm_id].to_s
130
+ raise ArgumentError, 'swarm_id is required' if sid.empty?
131
+
132
+ FileUtils.mkdir_p(File.join(SWARM_ROOT, sid))
133
+ entry = {
134
+ ts: Time.now.utc.iso8601,
135
+ from: opts[:from].to_s,
136
+ to: (opts[:to] || :all).to_s,
137
+ content: opts[:content].to_s
138
+ }
139
+ File.open(bus_path(swarm_id: sid), 'a') { |f| f.puts(JSON.generate(entry)) }
140
+ entry
141
+ end
142
+
143
+ # Supported Method Parameters::
144
+ # msgs = PWN::AI::Agent::Swarm.bus_tail(swarm_id: 'required', limit: 12)
145
+
146
+ public_class_method def self.bus_tail(opts = {})
147
+ sid = opts[:swarm_id].to_s
148
+ limit = (opts[:limit] || DEFAULT_TAIL).to_i
149
+ bp = bus_path(swarm_id: sid)
150
+ return [] unless File.exist?(bp)
151
+
152
+ File.readlines(bp).last(limit).map { |l| JSON.parse(l, symbolize_names: true) }
153
+ rescue StandardError
154
+ []
155
+ end
156
+
157
+ # ------------------------------------------------------------------
158
+ # Core: run a single persona turn under Loop.run
159
+ # ------------------------------------------------------------------
160
+
161
+ # Supported Method Parameters::
162
+ # reply = PWN::AI::Agent::Swarm.ask(
163
+ # name: 'required - persona name from ~/.pwn/agents.yml',
164
+ # request: 'required - what to ask/instruct the persona',
165
+ # swarm_id: 'optional - join an existing swarm (created if omitted)',
166
+ # to: 'optional - addressee recorded on the bus (default :all)',
167
+ # on_tool: 'optional - ->(name, args, result) live-UI callback'
168
+ # )
169
+
170
+ public_class_method def self.ask(opts = {})
171
+ name = opts[:name].to_s
172
+ persona = personas[name.to_sym]
173
+ raise ArgumentError, "unknown persona: #{name} (see #{AGENTS_FILE})" unless persona
174
+
175
+ sid = opts[:swarm_id] || create(topic: opts[:request].to_s[0, 60])[:swarm_id]
176
+ depth = Thread.current[:pwn_swarm_depth] || 0
177
+ if depth >= max_depth
178
+ raise "swarm recursion depth #{depth} >= max_depth #{max_depth} " \
179
+ '(PWN::Env[:ai][:agent][:max_depth])'
180
+ end
181
+
182
+ bus_append(swarm_id: sid, from: opts[:from] || caller_label,
183
+ to: name, content: opts[:request].to_s)
184
+
185
+ session_id = persona_session(swarm_id: sid, name: name)
186
+ sys = build_persona_prompt(name: name, persona: persona,
187
+ swarm_id: sid, session_id: session_id)
188
+
189
+ Thread.current[:pwn_swarm_depth] = depth + 1
190
+ reply = with_persona_env(persona: persona) do
191
+ Loop.run(
192
+ request: opts[:request].to_s,
193
+ session_id: session_id,
194
+ enabled_toolsets: persona[:toolsets],
195
+ system_role_content: sys,
196
+ on_tool: opts[:on_tool]
197
+ )
198
+ end
199
+
200
+ bus_append(swarm_id: sid, from: name, to: opts[:to] || :all, content: reply)
201
+ { swarm_id: sid, name: name, session_id: session_id, reply: reply }
202
+ ensure
203
+ Thread.current[:pwn_swarm_depth] = depth
204
+ end
205
+
206
+ # Supported Method Parameters::
207
+ # result = PWN::AI::Agent::Swarm.debate(
208
+ # names: 'required - Array of persona names, order = speaking order',
209
+ # topic: 'required - opening question / claim',
210
+ # rounds: 'optional - full passes over names (default 2)',
211
+ # swarm_id: 'optional - join an existing swarm',
212
+ # on_tool: 'optional - ->(name, args, result) live-UI callback'
213
+ # )
214
+
215
+ public_class_method def self.debate(opts = {})
216
+ names = Array(opts[:names]).map(&:to_s)
217
+ raise ArgumentError, 'names must contain at least 2 personas' if names.length < 2
218
+
219
+ topic = opts[:topic].to_s
220
+ raise ArgumentError, 'topic is required' if topic.strip.empty?
221
+
222
+ rounds = (opts[:rounds] || 2).to_i
223
+ sid = opts[:swarm_id] || create(topic: topic)[:swarm_id]
224
+
225
+ last_speaker = 'moderator'
226
+ last_msg = topic
227
+ transcript = []
228
+
229
+ rounds.times do |r|
230
+ names.each do |n|
231
+ req = if r.zero? && n == names.first
232
+ topic
233
+ else
234
+ "@#{last_speaker} said:\n#{last_msg}\n\n" \
235
+ 'Respond, critique, or advance the objective.'
236
+ end
237
+ res = ask(name: n, request: req, swarm_id: sid,
238
+ from: last_speaker, to: n, on_tool: opts[:on_tool])
239
+ transcript << { round: r + 1, name: n, reply: res[:reply] }
240
+ last_speaker = n
241
+ last_msg = res[:reply]
242
+ end
243
+ end
244
+
245
+ { swarm_id: sid, rounds: rounds, names: names,
246
+ transcript: transcript, bus: bus_path(swarm_id: sid) }
247
+ end
248
+
249
+ # Supported Method Parameters::
250
+ # result = PWN::AI::Agent::Swarm.broadcast(
251
+ # request: 'required', names: 'optional - default all personas',
252
+ # swarm_id: 'optional'
253
+ # )
254
+
255
+ public_class_method def self.broadcast(opts = {})
256
+ req = opts[:request].to_s
257
+ raise ArgumentError, 'request is required' if req.strip.empty?
258
+
259
+ names = Array(opts[:names]).map(&:to_s)
260
+ names = personas.keys.map(&:to_s) if names.empty?
261
+ sid = opts[:swarm_id] || create(topic: req[0, 60])[:swarm_id]
262
+
263
+ replies = names.to_h do |n|
264
+ [n, ask(name: n, request: req, swarm_id: sid,
265
+ from: 'broadcast', on_tool: opts[:on_tool])[:reply]]
266
+ end
267
+ { swarm_id: sid, replies: replies }
268
+ end
269
+
270
+ # ------------------------------------------------------------------
271
+ # privates
272
+ # ------------------------------------------------------------------
273
+
274
+ private_class_method def self.bus_path(opts = {})
275
+ File.join(SWARM_ROOT, opts[:swarm_id].to_s, 'bus.jsonl')
276
+ end
277
+
278
+ private_class_method def self.persona_session(opts = {})
279
+ sid = opts[:swarm_id].to_s
280
+ name = opts[:name].to_s
281
+ map_path = File.join(SWARM_ROOT, sid, 'personas.json')
282
+ map = File.exist?(map_path) ? JSON.parse(File.read(map_path)) : {}
283
+ return map[name] if map[name]
284
+
285
+ sess = PWN::Sessions.create(
286
+ title: "swarm:#{sid} persona:#{name}",
287
+ source: 'pwn-ai-swarm'
288
+ )
289
+ map[name] = sess[:id]
290
+ File.write(map_path, JSON.pretty_generate(map))
291
+ sess[:id]
292
+ end
293
+
294
+ private_class_method def self.build_persona_prompt(opts = {})
295
+ name = opts[:name]
296
+ persona = opts[:persona]
297
+ sid = opts[:swarm_id]
298
+ base = PromptBuilder.build(session_id: opts[:session_id])
299
+
300
+ bus = bus_tail(swarm_id: sid, limit: DEFAULT_TAIL).map do |m|
301
+ " [#{m[:ts]}] #{m[:from]} → #{m[:to]}: #{m[:content].to_s.tr("\n", ' ')[0, 400]}"
302
+ end.join("\n")
303
+
304
+ peers = (personas.keys.map(&:to_s) - [name]).join(', ')
305
+
306
+ <<~PROMPT
307
+ #{base}
308
+
309
+ SWARM
310
+ swarm_id : #{sid}
311
+ you_are : #{name}
312
+ peers : #{peers.empty? ? '(none)' : peers}
313
+ depth : #{Thread.current[:pwn_swarm_depth] || 0} / #{max_depth}
314
+ (Use agent_ask to delegate to a peer only if strictly necessary
315
+ and you have the 'swarm' toolset — depth is capped.)
316
+
317
+ PERSONA (#{name})
318
+ #{persona[:role]}
319
+
320
+ SWARM BUS (last #{DEFAULT_TAIL} msgs, newest last)
321
+ #{bus.empty? ? ' (empty)' : bus}
322
+ PROMPT
323
+ end
324
+
325
+ # Temporarily override PWN::Env[:ai][:active] and
326
+ # PWN::Env[:ai][:agent][:max_iters] for the duration of a sub-agent
327
+ # turn, restoring both afterwards even on raise.
328
+ private_class_method def self.with_persona_env(opts = {})
329
+ persona = opts[:persona]
330
+ ai = env_ai
331
+ return yield unless ai
332
+
333
+ agent_h = (ai[:agent] ||= {})
334
+ prev_active = ai[:active]
335
+ prev_iters = agent_h[:max_iters]
336
+
337
+ ai[:active] = persona[:engine].to_s if persona[:engine]
338
+ agent_h[:max_iters] = persona[:max_iters] if persona[:max_iters]
339
+ yield
340
+ ensure
341
+ if ai
342
+ ai[:active] = prev_active
343
+ agent_h[:max_iters] = prev_iters
344
+ end
345
+ end
346
+
347
+ private_class_method def self.normalize_persona(opts = {})
348
+ p = opts[:persona] || {}
349
+ {
350
+ role: p[:role].to_s,
351
+ engine: (p[:engine].to_s.empty? ? nil : p[:engine].to_s.downcase.to_sym),
352
+ toolsets: Array(p[:toolsets]).map(&:to_s).then { |a| a.empty? ? DEFAULT_TOOLSET.dup : a },
353
+ max_iters: (p[:max_iters] || DEFAULT_ITERS).to_i
354
+ }
355
+ end
356
+
357
+ private_class_method def self.max_depth
358
+ v = (PWN::Env.dig(:ai, :agent, :max_depth) if defined?(PWN::Env))
359
+ v.to_i.positive? ? v.to_i : DEFAULT_DEPTH
360
+ rescue StandardError
361
+ DEFAULT_DEPTH
362
+ end
363
+
364
+ private_class_method def self.env_ai
365
+ return nil unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
366
+
367
+ ai = PWN::Env[:ai]
368
+ ai.is_a?(Hash) && !ai.frozen? ? ai : nil
369
+ rescue StandardError
370
+ nil
371
+ end
372
+
373
+ private_class_method def self.caller_label
374
+ d = Thread.current[:pwn_swarm_depth] || 0
375
+ d.zero? ? 'orchestrator' : "depth#{d}"
376
+ end
377
+
378
+ private_class_method def self.deep_stringify(opts = {})
379
+ h = opts[:hash]
380
+ case h
381
+ when Hash then h.to_h { |k, v| [k.to_s, deep_stringify(hash: v)] }
382
+ when Array then h.map { |v| deep_stringify(hash: v) }
383
+ when Symbol then h.to_s
384
+ else h
385
+ end
386
+ end
387
+
388
+ # Author(s):: 0day Inc. <support@0dayinc.com>
389
+
390
+ public_class_method def self.authors
391
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
392
+ end
393
+
394
+ # Display Usage for this Module
395
+
396
+ public_class_method def self.help
397
+ puts <<~USAGE
398
+ USAGE:
399
+ # Define personas (or edit #{AGENTS_FILE} directly)
400
+ PWN::AI::Agent::Swarm.spawn(
401
+ name: 'red',
402
+ role: 'Offensive researcher. Propose the most likely exploit path...',
403
+ toolsets: %w[terminal pwn memory extrospection],
404
+ engine: :anthropic
405
+ )
406
+
407
+ # One-shot: ask a persona (creates a swarm if none given)
408
+ r = PWN::AI::Agent::Swarm.ask(name: 'red', request: 'Enumerate attack surface for target X')
409
+
410
+ # Antagonistic feedback loop
411
+ d = PWN::AI::Agent::Swarm.debate(
412
+ names: %w[red blue],
413
+ topic: 'Is CVE-2026-NNNN exploitable on target X?',
414
+ rounds: 3
415
+ )
416
+ puts d[:transcript].map { |t| "\#{t[:name]}: \#{t[:reply][0,200]}" }
417
+
418
+ # Fan-out
419
+ PWN::AI::Agent::Swarm.broadcast(request: 'Summarise findings so far')
420
+
421
+ # Inspect / resume cross-session
422
+ PWN::AI::Agent::Swarm.list
423
+ PWN::AI::Agent::Swarm.bus_tail(swarm_id: d[:swarm_id], limit: 50)
424
+
425
+ Config:
426
+ PWN::Env[:ai][:agent][:max_depth] # recursion cap (default #{DEFAULT_DEPTH})
427
+ persona[:max_iters] # per-turn iteration cap (default #{DEFAULT_ITERS})
428
+ persona[:toolsets] # Registry toolset allow-list; omit 'swarm'
429
+ # to prevent that persona spawning sub-agents.
430
+
431
+ #{self}.authors
432
+ USAGE
433
+ end
434
+ end
435
+ end
436
+ end
437
+ end