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,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+ require 'pwn/sessions'
5
+
6
+ # Thin wrappers around PWN::Sessions so the model can DISCOVER and INSPECT
7
+ # the JSONL transcripts that learning_reflect / learning_distill_skill
8
+ # consume. Without these the model has to blindly `shell("ls ~/.pwn/sessions")`
9
+ # and guess a session_id — this closes that gap in the learning loop.
10
+
11
+ PWN::AI::Agent::Registry.register(
12
+ name: 'sessions_list',
13
+ toolset: 'sessions',
14
+ schema: {
15
+ name: 'sessions_list',
16
+ description: 'List every pwn-ai session transcript in ~/.pwn/sessions ' \
17
+ '(id, path, size_bytes, mtime, line_count) newest-first. ' \
18
+ 'Use to discover a session_id for learning_reflect / ' \
19
+ 'learning_distill_skill / sessions_view.',
20
+ parameters: {
21
+ type: 'object',
22
+ properties: {
23
+ limit: { type: 'integer', default: 25, description: 'Max sessions to return (newest first).' }
24
+ },
25
+ required: []
26
+ }
27
+ },
28
+ check: -> { defined?(PWN::Sessions) },
29
+ handler: lambda { |args|
30
+ limit = (args[:limit] || 25).to_i
31
+ PWN::Sessions.list.sort_by { |s| s[:mtime].to_s }.reverse.first(limit)
32
+ }
33
+ )
34
+
35
+ PWN::AI::Agent::Registry.register(
36
+ name: 'sessions_view',
37
+ toolset: 'sessions',
38
+ schema: {
39
+ name: 'sessions_view',
40
+ description: 'Load a session transcript by id and return its entries ' \
41
+ '(role, timestamp, truncated content). Inspect BEFORE ' \
42
+ 'calling learning_reflect / learning_distill_skill on it.',
43
+ parameters: {
44
+ type: 'object',
45
+ properties: {
46
+ session_id: { type: 'string', description: 'PWN::Sessions id (basename without .jsonl).' },
47
+ max_entries: { type: 'integer', default: 200, description: 'Cap on transcript entries returned.' },
48
+ truncate: { type: 'integer', default: 400, description: 'Chars to keep per entry content.' }
49
+ },
50
+ required: %w[session_id]
51
+ }
52
+ },
53
+ check: -> { defined?(PWN::Sessions) },
54
+ handler: lambda { |args|
55
+ sid = args[:session_id].to_s
56
+ cap = (args[:max_entries] || 200).to_i
57
+ trunc = (args[:truncate] || 400).to_i
58
+ rows = PWN::Sessions.load(session_id: sid)
59
+ raise ArgumentError, "no such session: #{sid}" if rows.empty?
60
+
61
+ entries = rows.first(cap).map do |e|
62
+ {
63
+ role: e[:role],
64
+ timestamp: e[:timestamp],
65
+ content: e[:content].to_s[0, trunc]
66
+ }
67
+ end
68
+ { session_id: sid, total_entries: rows.length, returned: entries.length, entries: entries }
69
+ }
70
+ )
71
+
72
+ PWN::AI::Agent::Registry.register(
73
+ name: 'sessions_delete',
74
+ toolset: 'sessions',
75
+ schema: {
76
+ name: 'sessions_delete',
77
+ description: 'Delete a session transcript (~/.pwn/sessions/<id>.jsonl). ' \
78
+ 'Use to prune noisy / failed / dev-experiment transcripts ' \
79
+ 'so the reflect() corpus stays high-signal. Irreversible.',
80
+ parameters: {
81
+ type: 'object',
82
+ properties: {
83
+ session_id: { type: 'string' }
84
+ },
85
+ required: %w[session_id]
86
+ }
87
+ },
88
+ check: -> { defined?(PWN::Sessions) },
89
+ handler: lambda { |args|
90
+ sid = args[:session_id].to_s
91
+ raise ArgumentError, 'session_id is required' if sid.empty?
92
+
93
+ { session_id: sid, deleted: PWN::Sessions.delete(session_id: sid) }
94
+ }
95
+ )
96
+
97
+ PWN::AI::Agent::Registry.register(
98
+ name: 'sessions_stats',
99
+ toolset: 'sessions',
100
+ schema: {
101
+ name: 'sessions_stats',
102
+ description: 'Aggregate stats over all session transcripts: total_sessions, ' \
103
+ 'total_lines, oldest, newest, disk_bytes.',
104
+ parameters: { type: 'object', properties: {}, required: [] }
105
+ },
106
+ check: -> { defined?(PWN::Sessions) },
107
+ handler: lambda { |_args|
108
+ listing = PWN::Sessions.list
109
+ PWN::Sessions.stats.merge(
110
+ disk_bytes: listing.sum { |s| s[:size].to_i },
111
+ dir: PWN::Sessions.sessions_dir
112
+ )
113
+ }
114
+ )
115
+
116
+ PWN::AI::Agent::Registry.register(
117
+ name: 'sessions_current',
118
+ toolset: 'sessions',
119
+ schema: {
120
+ name: 'sessions_current',
121
+ description: 'Return the ACTIVE pwn-ai session_id (the one this ' \
122
+ 'conversation is being appended to). Pass it directly to ' \
123
+ 'learning_reflect / learning_distill_skill / sessions_view ' \
124
+ 'without copy-pasting from the system-prompt banner.',
125
+ parameters: { type: 'object', properties: {}, required: [] }
126
+ },
127
+ check: -> { defined?(PWN::Sessions) },
128
+ handler: lambda { |_args|
129
+ sid = nil
130
+ sid = PWN::Env.dig(:ai, :session_id) if defined?(PWN::Env) && PWN::Env.is_a?(Hash)
131
+ sid ||= (Pry.config.pwn_ai_session_id if defined?(Pry) && Pry.respond_to?(:config) && Pry.config.respond_to?(:pwn_ai_session_id))
132
+ path = sid ? File.join(PWN::Sessions.sessions_dir, "#{sid}.jsonl") : nil
133
+ {
134
+ session_id: sid,
135
+ path: path,
136
+ exists: (path && File.exist?(path)) || false
137
+ }
138
+ }
139
+ )
@@ -160,3 +160,33 @@ PWN::AI::Agent::Registry.register(
160
160
  }
161
161
  }
162
162
  )
163
+
164
+ PWN::AI::Agent::Registry.register(
165
+ name: 'skill_delete',
166
+ toolset: 'skills',
167
+ schema: {
168
+ name: 'skill_delete',
169
+ description: 'Delete a skill from ~/.pwn/skills/ by name. Use to prune ' \
170
+ 'low-quality auto-distilled skills so they stop appearing ' \
171
+ 'in every future system prompt. Irreversible.',
172
+ parameters: {
173
+ type: 'object',
174
+ properties: {
175
+ name: { type: 'string', description: 'Existing skill name (basename without extension).' }
176
+ },
177
+ required: %w[name]
178
+ }
179
+ },
180
+ check: -> { defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) },
181
+ handler: lambda { |args|
182
+ key = args[:name].to_s.to_sym
183
+ meta = PWN::Skills[key]
184
+ raise ArgumentError, "no such skill: #{key}" unless meta
185
+
186
+ path = meta[:path]
187
+ dir = File.dirname(path)
188
+ FileUtils.rm_f(path)
189
+ PWN::Config.load_skills(pwn_skills_path: dir)
190
+ { deleted: true, name: key, path: path, remaining: PWN::Skills.keys.length }
191
+ }
192
+ )
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+ require 'pwn/ai/agent/swarm'
5
+
6
+ # Multi-agent orchestration tools. These let the PRIMARY pwn-ai agent
7
+ # spawn/ask/debate SUB-agents (personas from ~/.pwn/agents.yml), each of
8
+ # which is a full PWN::AI::Agent::Loop.run — tool-calling, memory-aware,
9
+ # metrics-recorded, learning-reflected. This replaces the legacy pwn-irc
10
+ # "N chatbots on inspircd" mechanism with an in-process JSONL bus under
11
+ # ~/.pwn/swarm/<id>/, so conversations survive the process and can be
12
+ # resumed cross-session by any pwn-ai / PWN::Cron job.
13
+
14
+ PWN::AI::Agent::Registry.register(
15
+ name: 'agent_list',
16
+ toolset: 'swarm',
17
+ schema: {
18
+ name: 'agent_list',
19
+ description: 'List defined multi-agent personas from ~/.pwn/agents.yml ' \
20
+ '(name, role summary, engine, toolsets, max_iters). Use ' \
21
+ 'before agent_ask / agent_debate to see who you can ' \
22
+ 'delegate to. Define new ones with agent_spawn.',
23
+ parameters: { type: 'object', properties: {}, required: [] }
24
+ },
25
+ check: -> { defined?(PWN::AI::Agent::Swarm) },
26
+ handler: lambda { |_args|
27
+ PWN::AI::Agent::Swarm.personas.map do |name, p|
28
+ {
29
+ name: name,
30
+ role: p[:role].to_s[0, 200],
31
+ engine: p[:engine],
32
+ toolsets: p[:toolsets],
33
+ max_iters: p[:max_iters]
34
+ }
35
+ end
36
+ }
37
+ )
38
+
39
+ PWN::AI::Agent::Registry.register(
40
+ name: 'agent_spawn',
41
+ toolset: 'swarm',
42
+ schema: {
43
+ name: 'agent_spawn',
44
+ description: 'Define (or overwrite) a persona in ~/.pwn/agents.yml so ' \
45
+ 'it can be used with agent_ask / agent_debate. A persona ' \
46
+ 'is a system-role overlay + toolset allow-list + engine ' \
47
+ 'override. Omit "swarm" from toolsets to prevent that ' \
48
+ 'persona from recursively spawning further sub-agents.',
49
+ parameters: {
50
+ type: 'object',
51
+ properties: {
52
+ name: { type: 'string', description: 'snake_case persona name.' },
53
+ role: { type: 'string', description: 'System-role overlay describing this persona.' },
54
+ toolsets: {
55
+ type: 'array', items: { type: 'string' },
56
+ description: 'Registry toolset names this persona may use ' \
57
+ '(e.g. terminal, pwn, memory, skills, extrospection). ' \
58
+ 'Default: everything except swarm/cron.'
59
+ },
60
+ engine: {
61
+ type: 'string', enum: %w[openai anthropic grok gemini ollama],
62
+ description: 'Override AI engine for this persona (model diversity ' \
63
+ '= real antagonism). Default: inherit active engine.'
64
+ },
65
+ max_iters: {
66
+ type: 'integer',
67
+ description: 'Per-turn tool-loop cap for this persona (default 25).'
68
+ }
69
+ },
70
+ required: %w[name role]
71
+ }
72
+ },
73
+ check: -> { defined?(PWN::AI::Agent::Swarm) },
74
+ handler: lambda { |args|
75
+ PWN::AI::Agent::Swarm.spawn(
76
+ name: args[:name],
77
+ role: args[:role],
78
+ toolsets: args[:toolsets],
79
+ engine: args[:engine],
80
+ max_iters: args[:max_iters]
81
+ )
82
+ }
83
+ )
84
+
85
+ PWN::AI::Agent::Registry.register(
86
+ name: 'agent_ask',
87
+ toolset: 'swarm',
88
+ schema: {
89
+ name: 'agent_ask',
90
+ description: 'Run ONE turn of a named persona as a full tool-calling ' \
91
+ 'sub-agent (PWN::AI::Agent::Loop.run under that persona). ' \
92
+ 'The reply comes back to YOU as a tool result. Request + ' \
93
+ 'reply are appended to ~/.pwn/swarm/<swarm_id>/bus.jsonl so ' \
94
+ 'other personas (and future sessions) can see them. ' \
95
+ 'Recursion is depth-capped by PWN::Env[:ai][:agent][:max_depth].',
96
+ parameters: {
97
+ type: 'object',
98
+ properties: {
99
+ name: { type: 'string', description: 'Persona name (see agent_list).' },
100
+ request: { type: 'string', description: 'What to ask / instruct the persona.' },
101
+ swarm_id: {
102
+ type: 'string',
103
+ description: 'Existing swarm to join (from swarm_list / a prior ' \
104
+ 'agent_ask). Omit to auto-create a new swarm.'
105
+ }
106
+ },
107
+ required: %w[name request]
108
+ }
109
+ },
110
+ max_chars: 32_000,
111
+ check: -> { defined?(PWN::AI::Agent::Swarm) },
112
+ handler: lambda { |args|
113
+ PWN::AI::Agent::Swarm.ask(
114
+ name: args[:name],
115
+ request: args[:request],
116
+ swarm_id: args[:swarm_id]
117
+ )
118
+ }
119
+ )
120
+
121
+ PWN::AI::Agent::Registry.register(
122
+ name: 'agent_debate',
123
+ toolset: 'swarm',
124
+ schema: {
125
+ name: 'agent_debate',
126
+ description: 'Round-robin an antagonistic debate between 2+ personas. ' \
127
+ 'Each persona sees the swarm bus tail (prior turns) and is ' \
128
+ 'asked to respond/critique/advance. Returns the full ' \
129
+ 'transcript plus swarm_id so you can continue it later. ' \
130
+ 'This is the native replacement for the legacy pwn-irc ' \
131
+ 'multi-agent chat.',
132
+ parameters: {
133
+ type: 'object',
134
+ properties: {
135
+ names: {
136
+ type: 'array', items: { type: 'string' },
137
+ description: '>=2 persona names in speaking order.'
138
+ },
139
+ topic: { type: 'string', description: 'Opening question / claim / target.' },
140
+ rounds: { type: 'integer', default: 2, description: 'Full passes over names.' },
141
+ swarm_id: { type: 'string', description: 'Existing swarm to continue.' }
142
+ },
143
+ required: %w[names topic]
144
+ }
145
+ },
146
+ max_chars: 48_000,
147
+ check: -> { defined?(PWN::AI::Agent::Swarm) },
148
+ handler: lambda { |args|
149
+ PWN::AI::Agent::Swarm.debate(
150
+ names: args[:names],
151
+ topic: args[:topic],
152
+ rounds: args[:rounds],
153
+ swarm_id: args[:swarm_id]
154
+ )
155
+ }
156
+ )
157
+
158
+ PWN::AI::Agent::Registry.register(
159
+ name: 'agent_broadcast',
160
+ toolset: 'swarm',
161
+ schema: {
162
+ name: 'agent_broadcast',
163
+ description: 'Fan the same request out to multiple personas (default: ' \
164
+ 'all defined) and return {name => reply}. Useful for ' \
165
+ 'ensemble opinions / voting on an approach.',
166
+ parameters: {
167
+ type: 'object',
168
+ properties: {
169
+ request: { type: 'string' },
170
+ names: { type: 'array', items: { type: 'string' } },
171
+ swarm_id: { type: 'string' }
172
+ },
173
+ required: %w[request]
174
+ }
175
+ },
176
+ max_chars: 48_000,
177
+ check: -> { defined?(PWN::AI::Agent::Swarm) },
178
+ handler: lambda { |args|
179
+ PWN::AI::Agent::Swarm.broadcast(
180
+ request: args[:request],
181
+ names: args[:names],
182
+ swarm_id: args[:swarm_id]
183
+ )
184
+ }
185
+ )
186
+
187
+ PWN::AI::Agent::Registry.register(
188
+ name: 'swarm_bus',
189
+ toolset: 'swarm',
190
+ schema: {
191
+ name: 'swarm_bus',
192
+ description: 'Tail the JSONL message bus for a swarm ' \
193
+ '(~/.pwn/swarm/<id>/bus.jsonl). Read-side of agent_ask / ' \
194
+ 'agent_debate — use to inspect what personas said in a ' \
195
+ 'prior (or concurrent) session before continuing it.',
196
+ parameters: {
197
+ type: 'object',
198
+ properties: {
199
+ swarm_id: { type: 'string' },
200
+ limit: { type: 'integer', default: 25 }
201
+ },
202
+ required: %w[swarm_id]
203
+ }
204
+ },
205
+ check: -> { defined?(PWN::AI::Agent::Swarm) },
206
+ handler: lambda { |args|
207
+ PWN::AI::Agent::Swarm.bus_tail(
208
+ swarm_id: args[:swarm_id],
209
+ limit: args[:limit] || 25
210
+ )
211
+ }
212
+ )
213
+
214
+ PWN::AI::Agent::Registry.register(
215
+ name: 'swarm_list',
216
+ toolset: 'swarm',
217
+ schema: {
218
+ name: 'swarm_list',
219
+ description: 'List all swarms under ~/.pwn/swarm/ (swarm_id, message ' \
220
+ 'count, mtime). Pick one to resume with agent_ask / ' \
221
+ 'agent_debate(swarm_id:) — this is how cross-session ' \
222
+ 'multi-agent conversations continue without a daemon.',
223
+ parameters: { type: 'object', properties: {}, required: [] }
224
+ },
225
+ check: -> { defined?(PWN::AI::Agent::Swarm) },
226
+ handler: lambda { |_args|
227
+ PWN::AI::Agent::Swarm.list
228
+ }
229
+ )
@@ -20,7 +20,7 @@ module PWN
20
20
 
21
21
  system_role_content = "Being an expert penetration tester skilled in code analysis, debugging, and exploitation while stepping through JavaScript in a Chrome DevTools debugging session: 1. Your sole purpose is to analyze each JavaScript step and generate an Exploit Prediction Scoring System (EPSS) score between 0% - 100%. The step currently resides in this block of JavaScript:\n```\n#{source_to_review}\n```\n2. If the score is >= 75%, generate a JavaScript proof-of-concept that would allow a threat actor to directly exploit or target a user for exploitation (i.e. no self-exploit). 3. If the EPSS score is >= 75% also provide a code fix. *** If the EPSS score is < 75%, no explanations or summaries - just the EPSS score."
22
22
 
23
- PWN::AI::Introspection.reflect_on(
23
+ PWN::AI::Agent::Introspection.reflect_on(
24
24
  system_role_content: system_role_content,
25
25
  request: request,
26
26
  suppress_pii_warning: true
@@ -12,7 +12,7 @@ module PWN
12
12
  # 4. CVSS score, vector string, and first.org calculator URI
13
13
  # 5. CWE category, brief description, and CWE URI
14
14
  # 6. Relevant NIST 800-53 control
15
- # It leverages the PWN::AI::Introspection.reflect_on method. Defaults to Jira for existing workflow compatibility.
15
+ # It leverages the PWN::AI::Agent::Introspection.reflect_on method. Defaults to Jira for existing workflow compatibility.
16
16
  module VulnGen
17
17
  # Supported Method Parameters::
18
18
  # ai_analysis = PWN::AI::Agent::VulnGen.analyze(
@@ -60,7 +60,7 @@ module PWN
60
60
  6. NIST 800-53 Security Control that is impacted by this vulnerability.
61
61
  "
62
62
 
63
- analysis = PWN::AI::Introspection.reflect_on(
63
+ analysis = PWN::AI::Agent::Introspection.reflect_on(
64
64
  system_role_content: system_role_content,
65
65
  request: request,
66
66
  suppress_pii_warning: true
data/lib/pwn/ai/agent.rb CHANGED
@@ -25,6 +25,9 @@ module PWN
25
25
  autoload :Loop, 'pwn/ai/agent/loop'
26
26
  autoload :Metrics, 'pwn/ai/agent/metrics'
27
27
  autoload :Learning, 'pwn/ai/agent/learning'
28
+ autoload :Extrospection, 'pwn/ai/agent/extrospection'
29
+ autoload :Introspection, 'pwn/ai/agent/introspection'
30
+ autoload :Swarm, 'pwn/ai/agent/swarm'
28
31
 
29
32
  # Display a List of Every PWN::AI Module
30
33
 
@@ -155,7 +155,7 @@ module PWN
155
155
  # tool_choice: 'optional - "auto" | "none" | "required" | {type:"function", function:{name:..}}',
156
156
  # model: 'optional - overrides PWN::Env[:ai][:anthropic][:model]',
157
157
  # temp: 'optional - temperature (defaults to PWN::Env[:ai][:anthropic][:temp] || 1)',
158
- # max_tokens: 'optional - defaults to 4096',
158
+ # max_tokens: 'optional - defaults to PWN::Env[:ai][:anthropic][:max_tokens] || 128_000',
159
159
  # timeout: 'optional - seconds (default 900)',
160
160
  # spinner: 'optional - display spinner (default false)'
161
161
  # )
@@ -175,7 +175,7 @@ module PWN
175
175
 
176
176
  http_body = {
177
177
  model: model,
178
- max_tokens: opts[:max_tokens] || 4096,
178
+ max_tokens: opts[:max_tokens] || engine[:max_tokens] || 128_000,
179
179
  temperature: temp,
180
180
  messages: anth_messages
181
181
  }
@@ -284,7 +284,22 @@ module PWN
284
284
  # Anthropic /v1/messages response -> OpenAI chat/completions shape
285
285
  private_class_method def self.anthropic_resp_to_oa(opts = {})
286
286
  resp = opts[:response] ||= {}
287
- blocks = Array(resp[:content])
287
+ blocks = Array(resp[:content])
288
+ # When Anthropic hits max_tokens mid tool_use emission it returns the
289
+ # block with input:{} and stop_reason:"max_tokens". Dispatching that
290
+ # yields "ArgumentError: <param> is required" from every tool handler,
291
+ # and round-tripping it in _native_content without a matching
292
+ # tool_result 400s the next turn. Strip those artifacts here.
293
+ if resp[:stop_reason] == 'max_tokens'
294
+ truncated, blocks = blocks.partition do |b|
295
+ b[:type] == 'tool_use' && (b[:input].nil? || (b[:input].respond_to?(:empty?) && b[:input].empty?))
296
+ end
297
+ unless truncated.empty?
298
+ names = truncated.map { |b| b[:name] }.join(', ')
299
+ warn "[pwn-ai] Anthropic hit max_tokens mid tool_use (#{names}); " \
300
+ 'dropping partial call. Raise ai.anthropic.max_tokens in PWN::Env.'
301
+ end
302
+ end
288
303
  text = blocks.select { |b| b[:type] == 'text' }.map { |b| b[:text] }.join
289
304
  tool_calls = blocks.select { |b| b[:type] == 'tool_use' }.map do |b|
290
305
  {
@@ -375,7 +390,7 @@ module PWN
375
390
 
376
391
  http_body = {
377
392
  model: model,
378
- max_tokens: 4096,
393
+ max_tokens: engine[:max_tokens] || 128_000,
379
394
  temperature: temp,
380
395
  system: system_role_content,
381
396
  messages: []
data/lib/pwn/ai.rb CHANGED
@@ -9,7 +9,6 @@ module PWN
9
9
  autoload :Anthropic, 'pwn/ai/anthropic'
10
10
  autoload :Gemini, 'pwn/ai/gemini'
11
11
  autoload :Grok, 'pwn/ai/grok'
12
- autoload :Introspection, 'pwn/ai/introspection'
13
12
  autoload :Ollama, 'pwn/ai/ollama'
14
13
  autoload :OpenAI, 'pwn/ai/open_ai'
15
14
 
data/lib/pwn/config.rb CHANGED
@@ -74,6 +74,7 @@ module PWN
74
74
  model: 'optional - Anthropic model to use (e.g. claude-3-5-sonnet-20240620)',
75
75
  system_role_content: 'You are an ethically hacking Anthropic agent.',
76
76
  temp: 'optional - Anthropic temperature',
77
+ max_tokens: 'optional - Max output tokens per response (default 8192). Raise if tool calls truncate.',
77
78
  max_prompt_length: 200_000
78
79
  },
79
80
  gemini: {
@@ -87,7 +88,15 @@ module PWN
87
88
  agent: {
88
89
  native_tools: true,
89
90
  max_iters: 25,
91
+ # Swarm (agent_ask/agent_debate) sub-agent recursion cap
92
+ max_depth: 3,
93
+ # run PWN::AI::Agent::Learning.auto_reflect after every final answer
94
+ auto_reflect: true,
95
+ # also run PWN::AI::Agent::Extrospection.auto_extrospect from auto_reflect
96
+ auto_extrospect: false,
90
97
  toolsets: nil
98
+ # multi-agent personas : ~/.pwn/agents.yml (see PWN::AI::Agent::Swarm.help)
99
+ # swarm bus : ~/.pwn/swarm/<swarm_id>/bus.jsonl
91
100
  }
92
101
  },
93
102
  plugins: {
@@ -100,28 +109,6 @@ module PWN
100
109
  rpc_pass: 'bitcoin RPC Password'
101
110
  }
102
111
  },
103
- irc: {
104
- ui_nick: '_human_',
105
- shared_chan: '#pwn',
106
- ai_agent_nicks: {
107
- browser: {
108
- pwn_rb: '/opt/pwn/lib/pwn/plugins/transparent_browser.rb',
109
- system_role_content: 'You are a browser. You are a web browser that can be controlled by a human or AI agent'
110
- },
111
- nimjeh: {
112
- pwn_rb: '',
113
- system_role_content: 'You are a sarcastic hacker. You find software zero day vulnerabilities. This involves analyzing source code, race conditions, application binaries, and network protocols from an offensive security perspective.'
114
- },
115
- nmap: {
116
- pwn_rb: '/opt/pwn/lib/pwn/plugins/nmap_it.rb',
117
- system_role_content: 'You are a network scanner. You are a network scanner that can be controlled by a human or AI agent'
118
- },
119
- shodan: {
120
- pwn_rb: '/opt/pwn/lib/pwn/plugins/shodan.rb',
121
- system_role_content: 'You are a passive reconnaissance agent. You are a passive reconnaissance agent that can be controlled by a human or AI agent'
122
- }
123
- }
124
- },
125
112
  hunter: { api_key: 'hunter.how API Key' },
126
113
  jira_data_center: {
127
114
  base_uri: 'Jira Server Base API URI (e.g. https://jira.company.com/rest/api/latest)',
@@ -300,7 +287,7 @@ module PWN
300
287
  iv: iv
301
288
  )
302
289
 
303
- valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :introspection }.map(&:downcase)
290
+ valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :agent }.map(&:downcase)
304
291
 
305
292
  engine = env[:ai][:active].to_s.downcase.to_sym
306
293
  raise "ERROR: Unsupported AI Engine: #{engine} in #{pwn_env_path}. Supported AI Engines:\n#{valid_ai_engines.inspect}" unless valid_ai_engines.include?(engine)
data/lib/pwn/cron.rb CHANGED
@@ -71,7 +71,7 @@ module PWN
71
71
  # Executes the job (for pwn-ai prompt it will use current active AI engine
72
72
  # via PWN::AI::* but without full REPL hook unless in pwn-ai).
73
73
  public_class_method def self.run(opts = {})
74
- id = opts[:id]
74
+ id = opts[:id].to_s
75
75
  jobs = load_jobs
76
76
  job = jobs[id] || jobs.values.find { |j| j[:name] == id || j[:id] == id }
77
77
  raise "Job #{id} not found" unless job
@@ -134,7 +134,7 @@ module PWN
134
134
  # Supported Method Parameters::
135
135
  # PWN::Cron.remove(id:)
136
136
  public_class_method def self.remove(opts = {}) # rubocop:disable Naming/PredicateMethod
137
- id = opts[:id]
137
+ id = opts[:id].to_s
138
138
  jobs = load_jobs
139
139
  jobs.delete(id)
140
140
  save_jobs(jobs: jobs)
@@ -152,10 +152,10 @@ module PWN
152
152
  end
153
153
 
154
154
  # Install a crontab line that invokes this job via pwn
155
- # (assumes /opt/pwn and rvm ruby-4.0.1@pwn - user can edit crontab)
155
+ # (assumes /opt/pwn and the active rvm ruby@pwn gemset - user can edit crontab)
156
156
  public_class_method def self.install_crontab_entry(opts = {})
157
157
  job = opts[:job]
158
- cron_line = "#{job[:schedule]} cd /opt/pwn && /usr/local/rvm/bin/rvm ruby-4.0.1@pwn do ruby -I lib -e 'require \"pwn\"; PWN::Cron.run(id: \"#{job[:id]}\")' >> #{File.join(cron_dir, 'cron.log')} 2>&1"
158
+ cron_line = "#{job[:schedule]} cd /opt/pwn && /usr/local/rvm/bin/rvm ruby-#{RUBY_VERSION}@pwn do ruby -I lib -e 'require \"pwn\"; PWN::Cron.run(id: \"#{job[:id]}\")' >> #{File.join(cron_dir, 'cron.log')} 2>&1"
159
159
  # Append to user's crontab (non-destructive)
160
160
  existing = `crontab -l 2>/dev/null || true`
161
161
  unless existing.include?(job[:id])
@@ -169,8 +169,17 @@ module PWN
169
169
  FileUtils.mkdir_p(cron_dir)
170
170
  return {} unless File.exist?(JOBS_FILE)
171
171
 
172
- YAML.safe_load_file(JOBS_FILE, symbolize_names: true) || {}
173
- rescue StandardError
172
+ raw = YAML.safe_load_file(
173
+ JOBS_FILE,
174
+ permitted_classes: [Symbol, Time],
175
+ symbolize_names: true
176
+ ) || {}
177
+ # Normalize outer job-id keys to String (create() uses String ids;
178
+ # symbolize_names would otherwise turn them into Symbols on reload
179
+ # and break jobs[id] / jobs.delete(id) / toggle lookups).
180
+ raw.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
181
+ rescue StandardError => e
182
+ warn("[PWN::Cron] load_jobs failed: #{e.class}: #{e.message}")
174
183
  {}
175
184
  end
176
185
 
@@ -180,7 +189,7 @@ module PWN
180
189
  end
181
190
 
182
191
  private_class_method def self.toggle(opts = {})
183
- id = opts[:id]
192
+ id = opts[:id].to_s
184
193
  enabled = opts[:enabled]
185
194
  jobs = load_jobs
186
195
  if jobs[id]