pwn 0.5.613 → 0.5.617
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.
- checksums.yaml +4 -4
- data/Gemfile +10 -10
- data/README.md +117 -17
- data/bin/pwn +47 -2
- data/bin/pwn_gqrx_scanner +75 -18
- data/documentation/AI-Integration.md +36 -0
- data/documentation/BurpSuite.md +26 -0
- data/documentation/Contributing.md +31 -0
- data/documentation/Diagrams.md +140 -0
- data/documentation/Drivers.md +25 -0
- data/documentation/General-PWN-Usage.md +60 -0
- data/documentation/Home.md +78 -0
- data/documentation/How-PWN-Works.md +62 -0
- data/documentation/Installation.md +75 -0
- data/documentation/NmapIt.md +18 -0
- data/documentation/Plugins.md +59 -0
- data/documentation/Reporting.md +28 -0
- data/documentation/SAST.md +32 -0
- data/documentation/Skills-Memory-Learning.md +44 -0
- data/documentation/Transparent-Browser.md +32 -0
- data/documentation/Troubleshooting.md +38 -0
- data/documentation/What-is-PWN.md +53 -0
- data/documentation/Why-PWN.md +48 -0
- data/documentation/diagrams/ai-integration-tool-calling.svg +172 -0
- data/documentation/diagrams/build.sh +20 -0
- data/documentation/diagrams/burp-vs-zap-preference.svg +69 -0
- data/documentation/diagrams/code-scanning-sast.svg +172 -0
- data/documentation/diagrams/dot/_THEME.md +27 -0
- data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
- data/documentation/diagrams/dot/ai-integration-tool-calling.dot +50 -0
- data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
- data/documentation/diagrams/dot/burp-vs-zap-preference.dot +26 -0
- data/documentation/diagrams/dot/code-scanning-sast.dot +49 -0
- data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
- data/documentation/diagrams/dot/driver-framework.dot +39 -0
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
- data/documentation/diagrams/dot/fuzzing-workflow.dot +47 -0
- data/documentation/diagrams/dot/history-to-drivers.dot +34 -0
- data/documentation/diagrams/dot/memory-skills-detailed.dot +60 -0
- data/documentation/diagrams/dot/network-infra-testing.dot +54 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +115 -0
- data/documentation/diagrams/dot/penetration-testing-workflow.dot +60 -0
- data/documentation/diagrams/dot/plugin-ecosystem.dot +45 -0
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +98 -0
- data/documentation/diagrams/dot/pwn-repl-prototyping.dot +56 -0
- data/documentation/diagrams/dot/reporting-pipeline.dot +46 -0
- data/documentation/diagrams/dot/reverse-engineering-flow.dot +43 -0
- data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
- data/documentation/diagrams/dot/sessions-cron-automation.dot +44 -0
- data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
- data/documentation/diagrams/dot/web-application-testing.dot +49 -0
- data/documentation/diagrams/dot/zero-day-research-flow.dot +49 -0
- data/documentation/diagrams/driver-framework.svg +111 -0
- data/documentation/diagrams/fuzzing-workflow.svg +160 -0
- data/documentation/diagrams/history-to-drivers.svg +119 -0
- data/documentation/diagrams/memory-skills-detailed.svg +202 -0
- data/documentation/diagrams/network-infra-testing.svg +183 -0
- data/documentation/diagrams/overall-pwn-architecture.svg +405 -0
- data/documentation/diagrams/penetration-testing-workflow.svg +201 -0
- data/documentation/diagrams/plugin-ecosystem.svg +276 -0
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +306 -0
- data/documentation/diagrams/pwn-repl-prototyping.svg +168 -0
- data/documentation/diagrams/reporting-pipeline.svg +151 -0
- data/documentation/diagrams/reverse-engineering-flow.svg +134 -0
- data/documentation/diagrams/sessions-cron-automation.svg +136 -0
- data/documentation/diagrams/web-application-testing.svg +173 -0
- data/documentation/diagrams/zero-day-research-flow.svg +153 -0
- data/documentation/pwn-REPL.md +33 -0
- data/documentation/pwn-ai-Agent.md +43 -0
- data/git_commit.sh +1 -1
- data/lib/pwn/ai/agent/assembly.rb +1 -1
- data/lib/pwn/ai/agent/btc.rb +1 -1
- data/lib/pwn/ai/agent/burp_suite.rb +1 -1
- data/lib/pwn/ai/agent/extrospection.rb +618 -0
- data/lib/pwn/ai/agent/gqrx.rb +2 -2
- data/lib/pwn/ai/agent/hacker_one.rb +1 -1
- data/lib/pwn/ai/agent/introspection.rb +91 -0
- data/lib/pwn/ai/agent/learning.rb +379 -0
- data/lib/pwn/ai/agent/loop.rb +33 -3
- data/lib/pwn/ai/agent/metrics.rb +155 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +32 -3
- data/lib/pwn/ai/agent/sast.rb +1 -1
- data/lib/pwn/ai/agent/swarm.rb +437 -0
- data/lib/pwn/ai/agent/tools/cron.rb +163 -0
- data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
- data/lib/pwn/ai/agent/tools/learning.rb +221 -0
- data/lib/pwn/ai/agent/tools/memory.rb +27 -0
- data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
- data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
- data/lib/pwn/ai/agent/tools/skills.rb +124 -8
- data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
- data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
- data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
- data/lib/pwn/ai/agent.rb +5 -0
- data/lib/pwn/ai/anthropic.rb +19 -4
- data/lib/pwn/ai/grok.rb +249 -106
- data/lib/pwn/ai.rb +0 -1
- data/lib/pwn/config.rb +96 -36
- data/lib/pwn/cron.rb +16 -7
- data/lib/pwn/plugins/monkey_patch.rb +2 -2
- data/lib/pwn/plugins/repl.rb +158 -284
- data/lib/pwn/sdr/decoder/base.rb +251 -0
- data/lib/pwn/sdr/decoder/gsm.rb +84 -185
- data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
- data/lib/pwn/sdr/decoder.rb +1 -0
- data/lib/pwn/sdr/gqrx.rb +446 -65
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +68 -0
- data/spec/lib/pwn/ai/agent/metrics_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
- data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
- data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
- data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +30 -0
- data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
- data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
- data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
- data/third_party/pwn_rdoc.jsonl +178 -33
- metadata +110 -36
- data/documentation/PWN.png +0 -0
- data/documentation/PWN_Contributors_and_Users.png +0 -0
- data/documentation/fax-spectrogram.png +0 -0
- data/documentation/fax-waveform.png +0 -0
- data/documentation/frozen_string_literal_files_changed_from_false_to_true.txt +0 -54
- data/documentation/lifecycle_authz_replay.example.yaml +0 -27
- data/documentation/pwn_android_war_dialer_session.png +0 -0
- data/documentation/pwn_install.png +0 -0
- data/documentation/pwn_wallpaper.jpg +0 -0
- data/documentation/ringing-spectrogram.png +0 -0
- data/documentation/ringing-waveform.png +0 -0
- data/documentation/vulnerability_report_template.md +0 -37
- data/lib/pwn/ai/introspection.rb +0 -76
|
@@ -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}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`
|
|
@@ -70,15 +70,44 @@ module PWN
|
|
|
70
70
|
return '' unless defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) && !PWN::Skills.empty?
|
|
71
71
|
|
|
72
72
|
lines = PWN::Skills.map do |name, meta|
|
|
73
|
-
first = meta[:content].to_s.lines.first.to_s.strip
|
|
73
|
+
first = meta[:content].to_s.lines.reject { |l| l.strip.empty? || l.start_with?('---') }.first.to_s.strip
|
|
74
74
|
first = first[0, 100]
|
|
75
|
-
|
|
75
|
+
rc = Array(meta[:references]).length
|
|
76
|
+
ref_tag = rc.positive? ? " [#{rc} refs]" : ''
|
|
77
|
+
" - #{name}: #{first}#{ref_tag}"
|
|
76
78
|
end
|
|
77
79
|
"SKILLS (call skill_view to read full body)\n#{lines.join("\n")}\n\n"
|
|
78
80
|
rescue StandardError
|
|
79
81
|
''
|
|
80
82
|
end
|
|
81
83
|
|
|
84
|
+
private_class_method def self.learning_block
|
|
85
|
+
return '' unless defined?(PWN::AI::Agent::Learning)
|
|
86
|
+
|
|
87
|
+
ctx = PWN::AI::Agent::Learning.to_context(limit: 5).to_s
|
|
88
|
+
ctx.strip.empty? ? '' : "LEARNING\n#{ctx}"
|
|
89
|
+
rescue StandardError
|
|
90
|
+
''
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private_class_method def self.metrics_block
|
|
94
|
+
return '' unless defined?(PWN::AI::Agent::Metrics)
|
|
95
|
+
|
|
96
|
+
ctx = PWN::AI::Agent::Metrics.to_context(limit: 8).to_s
|
|
97
|
+
ctx.strip.empty? ? '' : ctx
|
|
98
|
+
rescue StandardError
|
|
99
|
+
''
|
|
100
|
+
end
|
|
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
|
+
|
|
82
111
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
83
112
|
|
|
84
113
|
public_class_method def self.authors
|
data/lib/pwn/ai/agent/sast.rb
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pwn/ai/agent/registry'
|
|
4
|
+
require 'pwn/cron'
|
|
5
|
+
|
|
6
|
+
# Thin wrappers around PWN::Cron so the model can schedule, inspect, run,
|
|
7
|
+
# enable/disable and remove recurring pwn-ai jobs (~/.pwn/cron/jobs.yml).
|
|
8
|
+
# Jobs may carry a pwn-ai `prompt`, a `ruby` snippet, or an external
|
|
9
|
+
# `script` path; system-cron installation is opt-in via :install_crontab.
|
|
10
|
+
|
|
11
|
+
PWN::AI::Agent::Registry.register(
|
|
12
|
+
name: 'cron_list',
|
|
13
|
+
toolset: 'cron',
|
|
14
|
+
schema: {
|
|
15
|
+
name: 'cron_list',
|
|
16
|
+
description: 'List all pwn-ai cron jobs from ~/.pwn/cron/jobs.yml ' \
|
|
17
|
+
'(id, name, schedule, enabled, last_run, last_status).',
|
|
18
|
+
parameters: { type: 'object', properties: {}, required: [] }
|
|
19
|
+
},
|
|
20
|
+
check: -> { defined?(PWN::Cron) },
|
|
21
|
+
handler: lambda { |_args|
|
|
22
|
+
PWN::Cron.list.map do |id, job|
|
|
23
|
+
{
|
|
24
|
+
id: id,
|
|
25
|
+
name: job[:name],
|
|
26
|
+
schedule: job[:schedule],
|
|
27
|
+
enabled: job[:enabled],
|
|
28
|
+
delivery: job[:delivery],
|
|
29
|
+
prompt: job[:prompt].to_s[0, 120],
|
|
30
|
+
ruby: job[:ruby].to_s[0, 120],
|
|
31
|
+
script: job[:script],
|
|
32
|
+
last_run: job[:last_run],
|
|
33
|
+
last_status: job[:last_status]
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
PWN::AI::Agent::Registry.register(
|
|
40
|
+
name: 'cron_create',
|
|
41
|
+
toolset: 'cron',
|
|
42
|
+
schema: {
|
|
43
|
+
name: 'cron_create',
|
|
44
|
+
description: 'Create a scheduled pwn-ai job. Provide exactly one of ' \
|
|
45
|
+
'prompt (sent to the active AI engine), ruby (evaluated ' \
|
|
46
|
+
'in-process), or script (external path). Set ' \
|
|
47
|
+
'install_crontab:true to also append a system crontab ' \
|
|
48
|
+
'entry that invokes PWN::Cron.run for this job id.',
|
|
49
|
+
parameters: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
name: { type: 'string', description: 'Human-friendly job name (default job-<id>).' },
|
|
53
|
+
schedule: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'Cron expression, e.g. "0 * * * *". Required when ' \
|
|
56
|
+
'install_crontab is true; otherwise informational.'
|
|
57
|
+
},
|
|
58
|
+
prompt: { type: 'string', description: 'pwn-ai prompt to run against the active engine.' },
|
|
59
|
+
ruby: { type: 'string', description: 'Ruby snippet evaluated in TOPLEVEL_BINDING.' },
|
|
60
|
+
script: { type: 'string', description: 'Path to an external executable script.' },
|
|
61
|
+
delivery: { type: 'string', enum: %w[log stdout], default: 'log' },
|
|
62
|
+
enabled: { type: 'boolean', default: true },
|
|
63
|
+
install_crontab: {
|
|
64
|
+
type: 'boolean',
|
|
65
|
+
default: false,
|
|
66
|
+
description: 'Also append a `crontab -l` entry that runs this job id via pwn.'
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
required: %w[schedule]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
check: -> { defined?(PWN::Cron) },
|
|
73
|
+
handler: lambda { |args|
|
|
74
|
+
PWN::Cron.create(
|
|
75
|
+
name: args[:name],
|
|
76
|
+
schedule: args[:schedule],
|
|
77
|
+
prompt: args[:prompt],
|
|
78
|
+
ruby: args[:ruby],
|
|
79
|
+
script: args[:script],
|
|
80
|
+
delivery: args[:delivery],
|
|
81
|
+
enabled: args.fetch(:enabled, true),
|
|
82
|
+
install_crontab: args[:install_crontab]
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
PWN::AI::Agent::Registry.register(
|
|
88
|
+
name: 'cron_run',
|
|
89
|
+
toolset: 'cron',
|
|
90
|
+
schema: {
|
|
91
|
+
name: 'cron_run',
|
|
92
|
+
description: 'Execute a cron job immediately by id (or name) and ' \
|
|
93
|
+
'return { job:, result:, duration:, status: }. Updates ' \
|
|
94
|
+
'last_run / last_status in jobs.yml.',
|
|
95
|
+
parameters: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
id: { type: 'string', description: 'Job id or job name.' }
|
|
99
|
+
},
|
|
100
|
+
required: %w[id]
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
check: -> { defined?(PWN::Cron) },
|
|
104
|
+
handler: lambda { |args|
|
|
105
|
+
PWN::Cron.run(id: args[:id])
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
PWN::AI::Agent::Registry.register(
|
|
110
|
+
name: 'cron_enable',
|
|
111
|
+
toolset: 'cron',
|
|
112
|
+
schema: {
|
|
113
|
+
name: 'cron_enable',
|
|
114
|
+
description: 'Set enabled:true on a cron job by id.',
|
|
115
|
+
parameters: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: { id: { type: 'string' } },
|
|
118
|
+
required: %w[id]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
check: -> { defined?(PWN::Cron) },
|
|
122
|
+
handler: lambda { |args|
|
|
123
|
+
{ id: args[:id], job: PWN::Cron.enable(id: args[:id]) }
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
PWN::AI::Agent::Registry.register(
|
|
128
|
+
name: 'cron_disable',
|
|
129
|
+
toolset: 'cron',
|
|
130
|
+
schema: {
|
|
131
|
+
name: 'cron_disable',
|
|
132
|
+
description: 'Set enabled:false on a cron job by id.',
|
|
133
|
+
parameters: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: { id: { type: 'string' } },
|
|
136
|
+
required: %w[id]
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
check: -> { defined?(PWN::Cron) },
|
|
140
|
+
handler: lambda { |args|
|
|
141
|
+
{ id: args[:id], job: PWN::Cron.disable(id: args[:id]) }
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
PWN::AI::Agent::Registry.register(
|
|
146
|
+
name: 'cron_remove',
|
|
147
|
+
toolset: 'cron',
|
|
148
|
+
schema: {
|
|
149
|
+
name: 'cron_remove',
|
|
150
|
+
description: 'Delete a cron job by id from ~/.pwn/cron/jobs.yml. ' \
|
|
151
|
+
'Does NOT scrub any system crontab entry — remove that ' \
|
|
152
|
+
'manually with `crontab -e` if install_crontab was used.',
|
|
153
|
+
parameters: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: { id: { type: 'string' } },
|
|
156
|
+
required: %w[id]
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
check: -> { defined?(PWN::Cron) },
|
|
160
|
+
handler: lambda { |args|
|
|
161
|
+
{ id: args[:id], removed: PWN::Cron.remove(id: args[:id]) }
|
|
162
|
+
}
|
|
163
|
+
)
|