pwn 0.5.603 → 0.5.609

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/Gemfile +7 -7
  4. data/README.md +3 -3
  5. data/git_commit.sh +9 -2
  6. data/lib/pwn/ai/agent/dispatch.rb +81 -0
  7. data/lib/pwn/ai/agent/loop.rb +209 -0
  8. data/lib/pwn/ai/agent/prompt_builder.rb +102 -0
  9. data/lib/pwn/ai/agent/registry.rb +149 -0
  10. data/lib/pwn/ai/agent/result.rb +105 -0
  11. data/lib/pwn/ai/agent/tools/memory.rb +75 -0
  12. data/lib/pwn/ai/agent/tools/ruby_eval.rb +45 -0
  13. data/lib/pwn/ai/agent/tools/shell.rb +44 -0
  14. data/lib/pwn/ai/agent/tools/skills.rb +76 -0
  15. data/lib/pwn/ai/agent.rb +13 -0
  16. data/lib/pwn/ai/anthropic.rb +202 -4
  17. data/lib/pwn/ai/gemini.rb +458 -0
  18. data/lib/pwn/ai/grok.rb +136 -15
  19. data/lib/pwn/ai/introspection.rb +9 -0
  20. data/lib/pwn/ai/ollama.rb +57 -4
  21. data/lib/pwn/ai/open_ai.rb +158 -131
  22. data/lib/pwn/ai.rb +7 -0
  23. data/lib/pwn/aws.rb +6 -0
  24. data/lib/pwn/blockchain/btc.rb +13 -8
  25. data/lib/pwn/blockchain.rb +6 -0
  26. data/lib/pwn/bounty/lifecycle_authz_replay.rb +26 -24
  27. data/lib/pwn/bounty.rb +6 -0
  28. data/lib/pwn/config.rb +34 -8
  29. data/lib/pwn/cron.rb +17 -12
  30. data/lib/pwn/ffi.rb +6 -0
  31. data/lib/pwn/memory.rb +11 -9
  32. data/lib/pwn/plugins/burp_suite.rb +3 -3
  33. data/lib/pwn/plugins/char.rb +4 -3
  34. data/lib/pwn/plugins/jenkins.rb +1 -1
  35. data/lib/pwn/plugins/oauth2.rb +2 -2
  36. data/lib/pwn/plugins/open_api.rb +82 -65
  37. data/lib/pwn/plugins/pony.rb +49 -45
  38. data/lib/pwn/plugins/repl.rb +232 -54
  39. data/lib/pwn/plugins/vault.rb +2 -2
  40. data/lib/pwn/plugins/vin.rb +6 -4
  41. data/lib/pwn/plugins/xxd.rb +4 -4
  42. data/lib/pwn/plugins.rb +6 -0
  43. data/lib/pwn/reports.rb +6 -0
  44. data/lib/pwn/sast.rb +6 -0
  45. data/lib/pwn/sdr/decoder/gsm.rb +2 -2
  46. data/lib/pwn/sdr/decoder.rb +6 -0
  47. data/lib/pwn/sdr/gqrx.rb +31 -31
  48. data/lib/pwn/sdr.rb +18 -2
  49. data/lib/pwn/sessions.rb +1 -1
  50. data/lib/pwn/version.rb +1 -1
  51. data/lib/pwn/www.rb +6 -0
  52. data/spec/conventions_spec.rb +149 -0
  53. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +15 -0
  54. data/spec/lib/pwn/ai/agent/loop_spec.rb +15 -0
  55. data/spec/lib/pwn/ai/agent/prompt_builder_spec.rb +15 -0
  56. data/spec/lib/pwn/ai/agent/registry_spec.rb +15 -0
  57. data/spec/lib/pwn/ai/agent/result_spec.rb +15 -0
  58. data/spec/lib/pwn/ai/agent/tools/memory_spec.rb +10 -0
  59. data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +10 -0
  60. data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +10 -0
  61. data/spec/lib/pwn/ai/agent/tools/skills_spec.rb +10 -0
  62. data/spec/lib/pwn/ai/gemini_spec.rb +15 -0
  63. data/spec/lib/pwn/memory_spec.rb +1 -1
  64. metadata +36 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d9cef98cd11c0d946179ae3bee8e726c491ae0328455c8aaef8380fa392c3a8
4
- data.tar.gz: 6abdf135e81a2596166ddfae6ec93bb738e8eeeb60ed27a4517f7c061898dff8
3
+ metadata.gz: 136257739e1b8bd0279eb7d0fe5763237321f9248a944ddd30260e3687fb74d2
4
+ data.tar.gz: 8a7b62b9c7ae10c5fecfbea6c8868c25d342162a30c5c8e053de93227582a985
5
5
  SHA512:
6
- metadata.gz: 3be83f533d5311947b2bde851319956aa6cee5c8047e5d8aa5b03768c1db0052fa25211b34e096b0bd7858f8327fd1c02998000fb546624f79718c745f2a2d7f
7
- data.tar.gz: e164db59ece4dc7440047008346a10680642a3f647231f23a0330ef87fb3b3556b5147700bcb7f12483c9f8f3c81f2dc291bb23a01eb35db7a604eac5dca7036
6
+ metadata.gz: 8f8657d28da8b182b93ec5111f759a2a390a4856ea05056e6ef7007da798c12799febad57a21dff3a99486f7c8cc30b844aa1220edfac61adc93ad6ef03c832b
7
+ data.tar.gz: 3b3f9506e4f8d3fc0ac026d1b46921c79ff4980e4c750d192904e6edbc86cc406475441e5e4987890400aab982410c1d811726189f70700690cbcac14961f036
data/.rubocop_todo.yml CHANGED
@@ -70,6 +70,7 @@ Metrics/MethodLength:
70
70
  Naming/AccessorMethodName:
71
71
  Exclude:
72
72
  - 'lib/pwn/ai/anthropic.rb'
73
+ - 'lib/pwn/ai/gemini.rb'
73
74
  - 'lib/pwn/ai/grok.rb'
74
75
  - 'lib/pwn/ai/ollama.rb'
75
76
  - 'lib/pwn/ai/open_ai.rb'
data/Gemfile CHANGED
@@ -18,7 +18,7 @@ gem 'aws-sdk', '3.3.0'
18
18
  gem 'barby', '0.7.0'
19
19
  gem 'base32', '0.3.4'
20
20
  gem 'bitcoin-ruby', '0.0.20'
21
- gem 'brakeman', '8.0.4'
21
+ gem 'brakeman', '8.0.5'
22
22
  gem 'bson', '5.2.0'
23
23
  gem 'bundler', '>=4.0.14'
24
24
  gem 'bundler-audit', '>=0.9.3'
@@ -49,8 +49,8 @@ gem 'jwt', '3.2.0'
49
49
  gem 'libusb', '0.7.2'
50
50
  gem 'luhn', '3.0.0'
51
51
  gem 'mail', '2.9.0'
52
- gem 'mcp', '0.18.0'
53
- gem 'meshtastic', '0.0.163'
52
+ gem 'mcp', '0.20.0'
53
+ gem 'meshtastic', '0.0.165'
54
54
  gem 'metasm', '1.0.6'
55
55
  gem 'mongo', '2.24.1'
56
56
  gem 'msfrpc-client', '1.1.2'
@@ -77,11 +77,11 @@ gem 'rbvmomi2', '3.10.0'
77
77
  gem 'rdoc', '7.0.4'
78
78
  gem 'rest-client', '2.1.0'
79
79
  gem 'rex', '2.0.13'
80
- gem 'rmagick', '7.0.3'
80
+ gem 'rmagick', '7.0.4'
81
81
  gem 'rqrcode', '3.2.0'
82
82
  gem 'rspec', '3.13.2'
83
83
  gem 'rtesseract', '3.1.4'
84
- gem 'rubocop', '1.87.0'
84
+ gem 'rubocop', '1.88.0'
85
85
  gem 'rubocop-rake', '0.7.1'
86
86
  gem 'rubocop-rspec', '3.10.2'
87
87
  gem 'ruby-audio', '1.6.1'
@@ -89,8 +89,8 @@ gem 'ruby-nmap', '1.0.3'
89
89
  gem 'ruby-saml', '1.18.1'
90
90
  gem 'rvm', '1.11.3.9'
91
91
  gem 'savon', '2.17.2'
92
- gem 'selenium-devtools', '0.148.0'
93
- gem 'selenium-webdriver', '4.44.0'
92
+ gem 'selenium-devtools', '0.149.0'
93
+ gem 'selenium-webdriver', '4.45.0'
94
94
  gem 'slack-ruby-client', '3.1.0'
95
95
  gem 'socksify', '1.8.1'
96
96
  gem 'spreadsheet', '1.3.5'
data/README.md CHANGED
@@ -37,7 +37,7 @@ $ cd /opt/pwn
37
37
  $ ./install.sh
38
38
  $ ./install.sh ruby-gem
39
39
  $ pwn
40
- pwn[v0.5.603]:001 >>> PWN.help
40
+ pwn[v0.5.609]:001 >>> PWN.help
41
41
  ```
42
42
 
43
43
  [![Installing the pwn Security Automation Framework](https://raw.githubusercontent.com/0dayInc/pwn/master/documentation/pwn_install.png)](https://youtu.be/G7iLUY4FzsI)
@@ -52,7 +52,7 @@ $ rvm use ruby-4.0.5@pwn
52
52
  $ gem uninstall --all --executables pwn
53
53
  $ gem install --verbose pwn
54
54
  $ pwn
55
- pwn[v0.5.603]:001 >>> PWN.help
55
+ pwn[v0.5.609]:001 >>> PWN.help
56
56
  ```
57
57
 
58
58
  If you're using a multi-user install of RVM do:
@@ -62,7 +62,7 @@ $ rvm use ruby-4.0.5@pwn
62
62
  $ rvmsudo gem uninstall --all --executables pwn
63
63
  $ rvmsudo gem install --verbose pwn
64
64
  $ pwn
65
- pwn[v0.5.603]:001 >>> PWN.help
65
+ pwn[v0.5.609]:001 >>> PWN.help
66
66
  ```
67
67
 
68
68
  PWN periodically upgrades to the latest version of Ruby which is reflected in `/opt/pwn/.ruby-version`. The easiest way to upgrade to the latest version of Ruby from a previous PWN installation is to run the following script:
data/git_commit.sh CHANGED
@@ -25,8 +25,15 @@ if (( $# == 3 )); then
25
25
 
26
26
  pwn_autoinc_version
27
27
  if [[ $? -ne 0 ]]; then
28
- echo 'ERROR: pwn_autoinc_version failed! Investigate and bump pwn version manually.'
29
- exit 1
28
+ echo 'ERROR: pwn_autoinc_version failed! Reinstalling pwn gemset...'
29
+ rvmsudo ./reinstall_pwn_gemset.sh
30
+ rvmsudo rake
31
+ rvmsudo rake install
32
+ if [[ $? -ne 0 ]]; then
33
+ echo 'ERROR: Attempt to reinstall pwn gemset failed! Please investigate and fix before trying again.'
34
+ exit 1
35
+ fi
36
+ pwn_autoinc_version
30
37
  fi
31
38
 
32
39
  # Generate RDoc JSONL for fine-tunning LLMs
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module PWN
6
+ module AI
7
+ module Agent
8
+ # Tool-call dispatch: takes a single tool_call object (OpenAI shape),
9
+ # looks up the registered handler, parses args, runs it, and returns a
10
+ # JSON string suitable for a role:'tool' message.
11
+ module Dispatch
12
+ # Supported Method Parameters::
13
+ # json_str = PWN::AI::Agent::Dispatch.call(
14
+ # tool_call: 'required - Hash { id:, type:, function: { name:, arguments: } }'
15
+ # )
16
+
17
+ public_class_method def self.call(opts = {})
18
+ tool_call = opts[:tool_call]
19
+ raise 'ERROR: tool_call is required' if tool_call.nil?
20
+
21
+ fn = tool_call[:function] || tool_call['function'] || {}
22
+ name = (fn[:name] || fn['name']).to_s
23
+ raw = fn[:arguments] || fn['arguments'] || '{}'
24
+
25
+ entry = Registry.lookup(name: name)
26
+ return JSON.generate(error: "unknown tool: #{name}") unless entry
27
+
28
+ args = parse_args(raw: raw)
29
+ result = entry.handler.call(args)
30
+ JSON.generate(success: true, result: result)
31
+ rescue StandardError => e
32
+ JSON.generate(
33
+ success: false,
34
+ error: "#{e.class}: #{e.message}",
35
+ backtrace: Array(e.backtrace).first(3)
36
+ )
37
+ end
38
+
39
+ private_class_method def self.parse_args(opts = {})
40
+ raw = opts[:raw]
41
+ case raw
42
+ when Hash then symbolize(hash: raw)
43
+ when String then raw.strip.empty? ? {} : JSON.parse(raw, symbolize_names: true)
44
+ when nil then {}
45
+ else symbolize(hash: raw.to_h)
46
+ end
47
+ rescue JSON::ParserError => e
48
+ raise ArgumentError, "invalid JSON arguments: #{e.message}"
49
+ end
50
+
51
+ private_class_method def self.symbolize(opts = {})
52
+ hash = opts[:hash] ||= {}
53
+ hash.each_with_object({}) { |(k, v), m| m[k.to_sym] = v }
54
+ end
55
+
56
+ # Author(s):: 0day Inc. <support@0dayinc.com>
57
+
58
+ public_class_method def self.authors
59
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
60
+ end
61
+
62
+ # Display Usage for this Module
63
+
64
+ public_class_method def self.help
65
+ puts <<~USAGE
66
+ USAGE:
67
+ json_str = PWN::AI::Agent::Dispatch.call(
68
+ tool_call: {
69
+ id: 'call_1',
70
+ type: 'function',
71
+ function: { name: 'shell', arguments: '{"command":"id"}' }
72
+ }
73
+ )
74
+
75
+ #{self}.authors
76
+ USAGE
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module PWN
6
+ module AI
7
+ module Agent
8
+ # The agent conversation loop:
9
+ #
10
+ # build system prompt → call LLM with tools → if tool_calls: dispatch,
11
+ # append role:'tool' results, loop → else: return text.
12
+ #
13
+ # This replaces the regex-ReAct in PWN::Plugins::REPL :pwn_ai_hook with
14
+ # native function-calling. State (memory, skills, sessions) is all
15
+ # externalised — Loop.run is stateless aside from the messages array it
16
+ # builds.
17
+ module Loop
18
+ DEFAULT_MAX_ITERS = 777
19
+
20
+ ENGINE_MODS = {
21
+ openai: 'PWN::AI::OpenAI',
22
+ grok: 'PWN::AI::Grok',
23
+ ollama: 'PWN::AI::Ollama',
24
+ anthropic: 'PWN::AI::Anthropic',
25
+ gemini: 'PWN::AI::Gemini'
26
+ }.freeze
27
+
28
+ # Supported Method Parameters::
29
+ # final = PWN::AI::Agent::Loop.run(
30
+ # user_text: 'required - what the human typed',
31
+ # session_id: 'optional - PWN::Sessions id (transcript is appended to it)',
32
+ # enabled_toolsets: 'optional - subset of Registry.toolsets, or nil for all',
33
+ # on_tool: 'optional - ->(name, args, result) callback for live UI'
34
+ # )
35
+
36
+ public_class_method def self.run(opts = {})
37
+ user_text = opts[:user_text].to_s
38
+ session_id = opts[:session_id]
39
+ on_tool = opts[:on_tool]
40
+
41
+ Registry.discover
42
+
43
+ tools = Registry.definitions(enabled: opts[:enabled_toolsets])
44
+ messages = [
45
+ { role: 'system', content: PromptBuilder.build(session_id: session_id) },
46
+ { role: 'user', content: user_text }
47
+ ]
48
+ append_session(session_id: session_id, role: 'user', content: user_text)
49
+
50
+ max_iters.times do |i|
51
+ msg = call_engine(messages: messages, tools: tools)
52
+ return '[pwn-ai] engine returned no message' if msg.nil?
53
+
54
+ messages << msg
55
+ calls = Array(msg[:tool_calls])
56
+
57
+ if calls.empty?
58
+ text = msg[:content].to_s
59
+ append_session(session_id: session_id, role: 'assistant', content: text)
60
+ return text
61
+ end
62
+
63
+ calls.each do |tc|
64
+ name = tc.dig(:function, :name).to_s
65
+ entry = Registry.lookup(name: name)
66
+ raw = Dispatch.call(tool_call: tc)
67
+ result = Result.condition(content: raw, entry: entry)
68
+
69
+ on_tool&.call(name, tc.dig(:function, :arguments), result)
70
+
71
+ messages << {
72
+ role: 'tool',
73
+ tool_call_id: tc[:id] || tc['id'] || "call_#{i}",
74
+ name: name,
75
+ content: result
76
+ }
77
+ append_session(session_id: session_id, role: 'tool', content: "#{name} → #{result[0, 400]}")
78
+ end
79
+ end
80
+
81
+ '[pwn-ai] iteration budget exhausted'
82
+ end
83
+
84
+ # Supported Method Parameters::
85
+ # msg = PWN::AI::Agent::Loop.call_engine(
86
+ # messages: 'required - OpenAI-format messages array',
87
+ # tools: 'optional - OpenAI tools array'
88
+ # )
89
+ #
90
+ # Returns a normalised assistant message hash:
91
+ # { role: 'assistant', content: String|nil,
92
+ # tool_calls: [ {id:, type:'function', function:{name:, arguments:}} ],
93
+ # _native_content: <provider raw> (when adapter needs round-trip) }
94
+
95
+ public_class_method def self.call_engine(opts = {})
96
+ messages = opts[:messages]
97
+ tools = opts[:tools]
98
+
99
+ engine = (PWN::Env.dig(:ai, :active) if defined?(PWN::Env)).to_s.downcase.to_sym
100
+ engine = :openai if engine == :''
101
+
102
+ mod_name = ENGINE_MODS[engine]
103
+ raise "ERROR: Unsupported AI engine for agent loop: #{engine}" unless mod_name
104
+
105
+ mod = Object.const_get(mod_name)
106
+ if mod.respond_to?(:chat_with_tools)
107
+ puts "MESSAGES:\n#{messages.inspect}\nTOOLS:\n#{tools.inspect}" if Pry.config.pwn_ai_debug
108
+ normalize_llm(
109
+ response: mod.chat_with_tools(
110
+ messages: messages,
111
+ tools: tools,
112
+ spinner: true
113
+ )
114
+ )
115
+ else
116
+ degrade_text_only(mod: mod, messages: messages)
117
+ end
118
+ end
119
+
120
+ # Supported Method Parameters::
121
+ # msg = PWN::AI::Agent::Loop.normalize_llm(
122
+ # response: 'required - chat_with_tools response Hash from any provider'
123
+ # )
124
+
125
+ public_class_method def self.normalize_llm(opts = {})
126
+ resp = opts[:response]
127
+ puts "RESPONSE: #{resp.inspect}" if Pry.config.pwn_ai_debug
128
+ return nil unless resp.is_a?(Hash)
129
+
130
+ msg = resp.dig(:choices, 0, :message) || resp[:assistant_message]
131
+ return nil unless msg
132
+
133
+ out = {
134
+ role: 'assistant',
135
+ content: msg[:content],
136
+ tool_calls: Array(msg[:tool_calls]).map do |tc|
137
+ {
138
+ id: tc[:id],
139
+ type: 'function',
140
+ function: {
141
+ name: tc.dig(:function, :name) || tc[:name],
142
+ arguments: tc.dig(:function, :arguments) || tc[:arguments]
143
+ }
144
+ }
145
+ end
146
+ }
147
+ # Preserve provider-native content blocks so chat can round-trip
148
+ # them exactly on the next iteration (e.g. Anthropic requires the
149
+ # original tool_use block to precede a tool_result).
150
+ out[:_native_content] = msg[:_native_content] if msg[:_native_content]
151
+ out
152
+ end
153
+
154
+ private_class_method def self.degrade_text_only(opts = {})
155
+ mod = opts[:mod]
156
+ messages = opts[:messages]
157
+
158
+ warn "[pwn-ai] #{mod} has no chat — falling back to text-only (no tool-calling)"
159
+ sys = messages.find { |m| m[:role] == 'system' }
160
+ user = messages.rfind { |m| m[:role] == 'user' }
161
+ r = mod.chat(request: user[:content], system_role_content: sys&.[](:content), spinner: true)
162
+ txt = r.is_a?(Hash) ? (r.dig(:choices, -1, :content) || r.dig(:choices, -1, :text)).to_s : r.to_s
163
+ { role: 'assistant', content: txt, tool_calls: [] }
164
+ end
165
+
166
+ private_class_method def self.max_iters
167
+ v = (PWN::Env.dig(:ai, :agent, :max_iters) if defined?(PWN::Env))
168
+ v.to_i.positive? ? v.to_i : DEFAULT_MAX_ITERS
169
+ rescue StandardError
170
+ DEFAULT_MAX_ITERS
171
+ end
172
+
173
+ private_class_method def self.append_session(opts = {})
174
+ session_id = opts[:session_id]
175
+ return unless session_id && defined?(PWN::Sessions)
176
+
177
+ PWN::Sessions.append(session_id: session_id, role: opts[:role], content: opts[:content])
178
+ rescue StandardError
179
+ nil
180
+ end
181
+
182
+ # Author(s):: 0day Inc. <support@0dayinc.com>
183
+
184
+ public_class_method def self.authors
185
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
186
+ end
187
+
188
+ # Display Usage for this Module
189
+
190
+ public_class_method def self.help
191
+ puts <<~USAGE
192
+ USAGE:
193
+ final = PWN::AI::Agent::Loop.run(
194
+ user_text: 'what does `id` return on this host?',
195
+ session_id: PWN::Sessions.create[:id],
196
+ enabled_toolsets: %w[terminal pwn memory skills],
197
+ on_tool: ->(name, args, result) { puts "→ \#{name}: \#{result[0,80]}" }
198
+ )
199
+
200
+ Supported engines: #{ENGINE_MODS.keys.join(', ')}
201
+ Set PWN::Env[:ai][:active] to choose; PWN::Env[:ai][:agent][:max_iters] to bound.
202
+
203
+ #{self}.authors
204
+ USAGE
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PWN
4
+ module AI
5
+ module Agent
6
+ # Assembles the system prompt for every Loop.run invocation from
7
+ # durable on-disk state: PWN::Env persona, host environment probe,
8
+ # PWN::Memory facts, and PWN::Skills index.
9
+ #
10
+ # Re-injection IS the persistence mechanism: this is rebuilt fresh on
11
+ # every user turn, so a memory_remember / skill_create from the prior
12
+ # turn shows up here with no extra wiring.
13
+ module PromptBuilder
14
+ # Supported Method Parameters::
15
+ # system_prompt = PWN::AI::Agent::PromptBuilder.build(
16
+ # session_id: 'optional - PWN::Sessions id to embed in the ENV block'
17
+ # )
18
+
19
+ public_class_method def self.build(opts = {})
20
+ session_id = opts[:session_id]
21
+ engine = active_engine
22
+ base = (PWN::Env.dig(:ai, engine, :system_role_content) if defined?(PWN::Env)) ||
23
+ 'You are an offensive-security AI named Sonny operating inside the pwn REPL.'
24
+
25
+ "
26
+ #{base}
27
+
28
+ ENVIRONMENT
29
+ host : #{host_line}
30
+ cwd : #{Dir.pwd}
31
+ ruby : #{RUBY_VERSION}
32
+ pwn : #{pwn_version}
33
+ session_id : #{session_id || '(none)'}
34
+
35
+ #{memory_block}#{skills_block}TOOL USE
36
+ Use the provided function tools to act on the host. A reply with
37
+ no tool_calls is treated as your FINAL answer to the user.
38
+ Prefer `pwn_eval` for anything in the PWN:: namespace and `shell`
39
+ for OS commands. Save durable facts with `memory_remember`.
40
+ "
41
+ end
42
+
43
+ private_class_method def self.active_engine
44
+ return :openai unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
45
+
46
+ PWN::Env.dig(:ai, :active).to_s.downcase.to_sym
47
+ rescue StandardError
48
+ :openai
49
+ end
50
+
51
+ private_class_method def self.host_line
52
+ `uname -srm 2>/dev/null`.strip
53
+ rescue StandardError
54
+ RUBY_PLATFORM
55
+ end
56
+
57
+ private_class_method def self.pwn_version
58
+ defined?(PWN::VERSION) ? PWN::VERSION : '?'
59
+ end
60
+
61
+ private_class_method def self.memory_block
62
+ return '' unless defined?(PWN::Memory) && PWN::Memory.respond_to?(:to_context)
63
+
64
+ ctx = PWN::Memory.to_context(limit: 25).to_s
65
+ ctx.strip.empty? ? '' : "MEMORY#{ctx}\n\n"
66
+ rescue StandardError
67
+ ''
68
+ end
69
+
70
+ private_class_method def self.skills_block
71
+ return '' unless defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) && !PWN::Skills.empty?
72
+
73
+ lines = PWN::Skills.map do |name, meta|
74
+ first = meta[:content].to_s.lines.first.to_s.strip
75
+ first = first[0, 100]
76
+ " - #{name}: #{first}"
77
+ end
78
+ "SKILLS (call skill_view to read full body)\n#{lines.join("\n")}\n\n"
79
+ rescue StandardError
80
+ ''
81
+ end
82
+
83
+ # Author(s):: 0day Inc. <support@0dayinc.com>
84
+
85
+ public_class_method def self.authors
86
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
87
+ end
88
+
89
+ # Display Usage for this Module
90
+
91
+ public_class_method def self.help
92
+ puts <<~USAGE
93
+ USAGE:
94
+ system_prompt = PWN::AI::Agent::PromptBuilder.build(session_id: 'abc')
95
+
96
+ #{self}.authors
97
+ USAGE
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PWN
4
+ module AI
5
+ module Agent
6
+ # Central registry for pwn-ai agent tools.
7
+ #
8
+ # Each file under lib/pwn/ai/agent/tools/*.rb calls
9
+ # +PWN::AI::Agent::Registry.register(...)+ at load time to declare a
10
+ # JSON-Schema (what the LLM sees) and a handler lambda (what pwn runs).
11
+ #
12
+ # Registry.definitions(...) returns the OpenAI-format +tools:+ array;
13
+ # Registry.lookup(name:) returns the entry for dispatch.
14
+ #
15
+ # Import chain (circular-import safe):
16
+ # agent/registry.rb (no deps on tool files)
17
+ # ^
18
+ # agent/tools/*.rb (require registry, call .register at top level)
19
+ # ^
20
+ # agent/loop.rb (calls Registry.discover then .definitions)
21
+ module Registry
22
+ Entry = Struct.new(
23
+ :name, # String - tool name exposed to the model
24
+ :toolset, # String - grouping for enable/disable (terminal, file, pwn, memory…)
25
+ :schema, # Hash - OpenAI function schema {name:, description:, parameters:}
26
+ :handler, # Proc - ->(args_hash) { ... } returning a JSON-serialisable object
27
+ :check, # Proc - -> { bool } gate; tool only advertised when truthy
28
+ :max_chars, # Integer - cap on serialised result before it re-enters the convo
29
+ keyword_init: true
30
+ )
31
+
32
+ @entries = {}
33
+ @discovered = false
34
+
35
+ # Supported Method Parameters::
36
+ # PWN::AI::Agent::Registry.register(
37
+ # name: 'required - tool name exposed to the model',
38
+ # toolset: 'required - grouping for enable/disable (terminal, file, pwn, memory…)',
39
+ # schema: 'required - OpenAI function schema {name:, description:, parameters:}',
40
+ # handler: 'required - ->(args_hash) { ... } returning a JSON-serialisable object',
41
+ # check: 'optional - -> { bool } gate; tool only advertised when truthy',
42
+ # max_chars: 'optional - cap on serialised result (default 24_000)'
43
+ # )
44
+
45
+ public_class_method def self.register(opts = {})
46
+ name = opts[:name].to_s
47
+ raise 'ERROR: name is required' if name.empty?
48
+ raise 'ERROR: schema is required' unless opts[:schema]
49
+ raise 'ERROR: handler is required' unless opts[:handler].respond_to?(:call)
50
+
51
+ @entries[name] = Entry.new(
52
+ name: name,
53
+ toolset: opts[:toolset].to_s,
54
+ schema: opts[:schema],
55
+ handler: opts[:handler],
56
+ check: opts[:check] ||= -> { true },
57
+ max_chars: opts[:max_chars] ||= 24_000
58
+ )
59
+ end
60
+
61
+ # Supported Method Parameters::
62
+ # entry = PWN::AI::Agent::Registry.lookup(
63
+ # name: 'required - registered tool name'
64
+ # )
65
+
66
+ public_class_method def self.lookup(opts = {})
67
+ name = opts[:name]
68
+ @entries[name.to_s]
69
+ end
70
+
71
+ # Supported Method Parameters::
72
+ # entries = PWN::AI::Agent::Registry.all
73
+
74
+ public_class_method def self.all
75
+ @entries.values
76
+ end
77
+
78
+ # Supported Method Parameters::
79
+ # names = PWN::AI::Agent::Registry.toolsets
80
+
81
+ public_class_method def self.toolsets
82
+ @entries.values.map(&:toolset).uniq.sort
83
+ end
84
+
85
+ # Supported Method Parameters::
86
+ # tools = PWN::AI::Agent::Registry.definitions(
87
+ # enabled: 'optional - Array of toolset names to include; nil = all whose check passes'
88
+ # )
89
+
90
+ public_class_method def self.definitions(opts = {})
91
+ enabled = opts[:enabled]
92
+ enabled = enabled.map(&:to_s) if enabled
93
+ @entries.values
94
+ .select { |e| (enabled.nil? || enabled.include?(e.toolset)) && safe_check(entry: e) }
95
+ .map { |e| { type: 'function', function: e.schema } }
96
+ end
97
+
98
+ # Supported Method Parameters::
99
+ # names = PWN::AI::Agent::Registry.discover(
100
+ # force: 'optional - re-require tool files even if already discovered (default false)'
101
+ # )
102
+
103
+ public_class_method def self.discover(opts = {})
104
+ force = opts[:force] ||= false
105
+ return @entries.keys if @discovered && !force
106
+
107
+ tools_dir = File.join(__dir__, 'tools')
108
+ if Dir.exist?(tools_dir)
109
+ Dir[File.join(tools_dir, '*.rb')].each do |f|
110
+ require f
111
+ rescue StandardError, LoadError => e
112
+ warn "[pwn-ai] failed to load tool #{File.basename(f)}: #{e.class}: #{e.message}"
113
+ end
114
+ end
115
+ @discovered = true
116
+ @entries.keys
117
+ end
118
+
119
+ private_class_method def self.safe_check(opts = {})
120
+ entry = opts[:entry]
121
+ entry.check.call
122
+ rescue StandardError
123
+ false
124
+ end
125
+
126
+ # Author(s):: 0day Inc. <support@0dayinc.com>
127
+
128
+ public_class_method def self.authors
129
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
130
+ end
131
+
132
+ # Display Usage for this Module
133
+
134
+ public_class_method def self.help
135
+ puts <<~USAGE
136
+ USAGE:
137
+ PWN::AI::Agent::Registry.discover
138
+ PWN::AI::Agent::Registry.definitions(enabled: %w[terminal pwn])
139
+ PWN::AI::Agent::Registry.lookup(name: 'shell') # => Entry
140
+ PWN::AI::Agent::Registry.toolsets # => ["memory","pwn","skills","terminal"]
141
+ PWN::AI::Agent::Registry.register(name:, toolset:, schema:, handler:)
142
+
143
+ #{self}.authors
144
+ USAGE
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end