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.
- checksums.yaml +4 -4
- data/README.md +109 -85
- data/README.md.bak +200 -0
- data/bin/pwn +47 -2
- data/bin/pwn_gqrx_scanner +75 -18
- data/documentation/AI-Integration.md +42 -26
- data/documentation/AWS.md +57 -0
- data/documentation/Agent-Tool-Registry.md +56 -0
- data/documentation/Banner.md +17 -0
- data/documentation/Blockchain.md +18 -0
- data/documentation/Bounty.md +21 -0
- data/documentation/BurpSuite.md +41 -16
- data/documentation/CLI-Drivers.md +58 -0
- data/documentation/Configuration.md +66 -0
- data/documentation/Contributing.md +33 -19
- data/documentation/Cron.md +47 -0
- data/documentation/Diagrams.md +123 -40
- data/documentation/Drivers.md +43 -16
- data/documentation/Extrospection.md +54 -0
- data/documentation/FFI.md +14 -0
- data/documentation/Fuzzing.md +36 -0
- data/documentation/General-PWN-Usage.md +50 -30
- data/documentation/Hardware.md +40 -0
- data/documentation/Home.md +78 -51
- data/documentation/How-PWN-Works.md +59 -57
- data/documentation/Installation.md +53 -28
- data/documentation/Metasploit.md +34 -0
- data/documentation/NmapIt.md +18 -12
- data/documentation/PWN.png +0 -0
- data/documentation/PWN_Contributors_and_Users.png +0 -0
- data/documentation/Persistence.md +38 -0
- data/documentation/Plugins.md +97 -47
- data/documentation/Reporting.md +25 -18
- data/documentation/SAST.md +39 -22
- data/documentation/SDR.md +40 -0
- data/documentation/Sessions.md +39 -0
- data/documentation/Skills-Memory-Learning.md +49 -29
- data/documentation/Swarm.md +71 -0
- data/documentation/Transparent-Browser.md +26 -22
- data/documentation/Troubleshooting.md +44 -25
- data/documentation/WWW.md +32 -0
- data/documentation/What-is-PWN.md +51 -32
- data/documentation/Why-PWN.md +41 -11
- data/documentation/diagrams/agent-tool-registry.svg +284 -0
- data/documentation/diagrams/ai-integration-tool-calling.svg +141 -103
- data/documentation/diagrams/aws-cloud-security.svg +166 -0
- data/documentation/diagrams/build.sh +20 -0
- data/documentation/diagrams/burp-vs-zap-preference.svg +47 -62
- data/documentation/diagrams/code-scanning-sast.svg +139 -79
- data/documentation/diagrams/cron-scheduling.svg +148 -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 +47 -20
- data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
- data/documentation/diagrams/dot/burp-vs-zap-preference.dot +23 -12
- data/documentation/diagrams/dot/code-scanning-sast.dot +46 -18
- data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
- data/documentation/diagrams/dot/driver-framework.dot +36 -14
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
- data/documentation/diagrams/dot/fuzzing-workflow.dot +44 -24
- data/documentation/diagrams/dot/hardware-hacking.dot +48 -0
- data/documentation/diagrams/dot/history-to-drivers.dot +31 -17
- data/documentation/diagrams/dot/memory-skills-detailed.dot +57 -25
- data/documentation/diagrams/dot/network-infra-testing.dot +51 -19
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +110 -44
- data/documentation/diagrams/dot/penetration-testing-workflow.dot +56 -25
- data/documentation/diagrams/dot/persistence-filesystem.dot +29 -0
- data/documentation/diagrams/dot/plugin-ecosystem.dot +41 -27
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +87 -46
- data/documentation/diagrams/dot/pwn-repl-prototyping.dot +49 -30
- data/documentation/diagrams/dot/reporting-pipeline.dot +43 -14
- data/documentation/diagrams/dot/reverse-engineering-flow.dot +40 -18
- data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
- data/documentation/diagrams/dot/sessions-cron-automation.dot +41 -14
- data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
- data/documentation/diagrams/dot/web-application-testing.dot +45 -23
- data/documentation/diagrams/dot/zero-day-research-flow.dot +46 -24
- data/documentation/diagrams/driver-framework.svg +87 -66
- data/documentation/diagrams/extrospection-world-awareness.svg +185 -0
- data/documentation/diagrams/fuzzing-workflow.svg +128 -115
- data/documentation/diagrams/hardware-hacking.svg +163 -0
- data/documentation/diagrams/history-to-drivers.svg +84 -82
- data/documentation/diagrams/memory-skills-detailed.svg +167 -119
- data/documentation/diagrams/network-infra-testing.svg +146 -84
- data/documentation/diagrams/overall-pwn-architecture.svg +356 -185
- data/documentation/diagrams/penetration-testing-workflow.svg +161 -108
- data/documentation/diagrams/persistence-filesystem.svg +174 -0
- data/documentation/diagrams/plugin-ecosystem.svg +225 -121
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +257 -185
- data/documentation/diagrams/pwn-repl-prototyping.svg +126 -137
- data/documentation/diagrams/reporting-pipeline.svg +128 -68
- data/documentation/diagrams/reverse-engineering-flow.svg +100 -80
- data/documentation/diagrams/sdr-radio-flow.svg +146 -0
- data/documentation/diagrams/sessions-cron-automation.svg +112 -68
- data/documentation/diagrams/swarm-multi-agent.svg +225 -0
- data/documentation/diagrams/web-application-testing.svg +136 -100
- data/documentation/diagrams/zero-day-research-flow.svg +116 -112
- data/documentation/fax-spectrogram.png +0 -0
- data/documentation/fax-waveform.png +0 -0
- data/documentation/pwn-REPL.md +40 -24
- data/documentation/pwn-ai-Agent.md +59 -30
- 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/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 +6 -4
- data/lib/pwn/ai/agent/loop.rb +15 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
- 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 +108 -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 +30 -0
- 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 +3 -0
- data/lib/pwn/ai/anthropic.rb +19 -4
- data/lib/pwn/ai.rb +0 -1
- data/lib/pwn/config.rb +10 -23
- data/lib/pwn/cron.rb +16 -7
- data/lib/pwn/plugins/repl.rb +90 -281
- 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/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/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 +199 -37
- metadata +65 -3
- 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
|
[]
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -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
|
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
|