pwn 0.5.667 → 0.5.668
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/.rubocop_todo.yml +1 -0
- data/Gemfile +4 -4
- data/lib/pwn/ai/agent/dispatch.rb +161 -0
- data/lib/pwn/ai/agent/extrospection.rb +9 -5
- data/lib/pwn/ai/agent/learning.rb +2 -1
- data/lib/pwn/ai/agent/loop.rb +311 -18
- data/lib/pwn/ai/agent/prompt_builder.rb +12 -5
- data/lib/pwn/ai/agent/reflect.rb +1 -0
- data/lib/pwn/ai/agent/registry.rb +1 -1
- data/lib/pwn/ai/agent/result.rb +3 -3
- data/lib/pwn/ai/agent/swarm.rb +1 -1
- data/lib/pwn/ai/agent/task_summarizer.rb +1 -0
- data/lib/pwn/ai/agent/tools/memory.rb +18 -4
- data/lib/pwn/ai/grok.rb +3 -0
- data/lib/pwn/ai/ollama.rb +131 -78
- data/lib/pwn/ai/open_ai.rb +3 -0
- data/lib/pwn/ai/open_web_ui.rb +715 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/config.rb +70 -36
- data/lib/pwn/memory.rb +140 -11
- data/lib/pwn/memory_index.rb +65 -19
- data/lib/pwn/plugins/repl.rb +6 -4
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reinforced_feedback_loop_spec.rb +8 -6
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +78 -0
- data/spec/lib/pwn/ai/agent/extrospection_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +117 -0
- data/spec/lib/pwn/ai/grok_spec.rb +5 -0
- data/spec/lib/pwn/ai/ollama_spec.rb +36 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +5 -0
- data/spec/lib/pwn/ai/open_web_ui_spec.rb +302 -0
- data/spec/lib/pwn/config_spec.rb +70 -0
- data/spec/lib/pwn/memory_spec.rb +63 -0
- data/third_party/pwn_rdoc.jsonl +32 -5
- metadata +11 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21fe6bb04741fbc78392d434588c1c86d0f605db0988bfa2192f68e9baa870ec
|
|
4
|
+
data.tar.gz: cf66359fb95bb12f08705d11dd304625878115902133d9e1ad0f8becfa8fb83a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35f079f6d7c765c6395f490f07c06ce3494b81487bba31c978367fc2b8a23a421a6b1d9a68cda02592fd94868ca343a13c441323b6e1dee0656f46ade8c88aef
|
|
7
|
+
data.tar.gz: b9a45751eaf30849ac1c98bd8d603df89d8522fb950dfc830f188769b0fc8e7b69848ed1f71053f7053da24a85e0c2f56f0f9a2c26b95de2040835a7e9871c47
|
data/.rubocop_todo.yml
CHANGED
data/Gemfile
CHANGED
|
@@ -67,7 +67,7 @@ gem 'os', '1.1.4'
|
|
|
67
67
|
gem 'ostruct', '0.6.3'
|
|
68
68
|
gem 'packetfu', '2.0.0'
|
|
69
69
|
gem 'packetgen', '4.1.1'
|
|
70
|
-
gem 'pdf-reader', '2.
|
|
70
|
+
gem 'pdf-reader', '2.16.0'
|
|
71
71
|
gem 'pg', '1.6.3'
|
|
72
72
|
gem 'pry', '0.16.0'
|
|
73
73
|
gem 'pry-doc', '1.7.0'
|
|
@@ -89,12 +89,12 @@ 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.4'
|
|
92
|
-
gem 'selenium-devtools', '0.
|
|
93
|
-
gem 'selenium-webdriver', '4.
|
|
92
|
+
gem 'selenium-devtools', '0.151.0'
|
|
93
|
+
gem 'selenium-webdriver', '4.47.0'
|
|
94
94
|
gem 'slack-ruby-client', '3.2.0'
|
|
95
95
|
gem 'socksify', '1.8.1'
|
|
96
96
|
gem 'spreadsheet', '1.3.5'
|
|
97
|
-
gem 'sqlite3', '2.9.
|
|
97
|
+
gem 'sqlite3', '2.9.6'
|
|
98
98
|
gem 'thin', '2.0.1'
|
|
99
99
|
gem 'tty-prompt', '0.23.1'
|
|
100
100
|
gem 'tty-spinner', '0.9.3'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'securerandom'
|
|
4
5
|
|
|
5
6
|
module PWN
|
|
6
7
|
module AI
|
|
@@ -118,6 +119,165 @@ module PWN
|
|
|
118
119
|
raise ArgumentError, "invalid JSON arguments: #{e.message}"
|
|
119
120
|
end
|
|
120
121
|
|
|
122
|
+
# Supported Method Parameters::
|
|
123
|
+
# calls = PWN::AI::Agent::Dispatch.tool_calls_from_text(
|
|
124
|
+
# text: 'required - assistant plain-text that may embed shell(...) / JSON tool forms'
|
|
125
|
+
# )
|
|
126
|
+
#
|
|
127
|
+
# Local / abliterated models often print tool invocations as content
|
|
128
|
+
# instead of native message.tool_calls. Supported shapes include:
|
|
129
|
+
# shell(command="id") / shell({"command":"id"}) / shell("id")
|
|
130
|
+
# {"name":"shell","arguments":{...}} / {"function":{"name":...}}
|
|
131
|
+
# {"tool":"shell","arguments":{...}} / {"call":"shell","arguments":{...}}
|
|
132
|
+
# call:shell{command: "uname -s"} / tool:shell{"command":"id"}
|
|
133
|
+
# When structured tool_calls are empty, Loop coerces those strings into
|
|
134
|
+
# OpenAI-shaped tool_call hashes so Dispatch runs them instead of
|
|
135
|
+
# treating the string as a FINAL answer.
|
|
136
|
+
|
|
137
|
+
public_class_method def self.tool_calls_from_text(opts = {})
|
|
138
|
+
text = opts[:text].to_s
|
|
139
|
+
return [] if text.strip.empty?
|
|
140
|
+
|
|
141
|
+
Registry.discover if defined?(Registry) && Registry.respond_to?(:discover)
|
|
142
|
+
known = if defined?(Registry)
|
|
143
|
+
Registry.all.map { |e| e.name.to_s }.reject(&:empty?)
|
|
144
|
+
else
|
|
145
|
+
%w[shell pwn_eval]
|
|
146
|
+
end
|
|
147
|
+
return [] if known.empty?
|
|
148
|
+
|
|
149
|
+
names_alt = known.map { |n| Regexp.escape(n) }.join('|')
|
|
150
|
+
calls = []
|
|
151
|
+
seen = {}
|
|
152
|
+
|
|
153
|
+
add = lambda do |name, args|
|
|
154
|
+
name = name.to_s
|
|
155
|
+
next unless known.include?(name)
|
|
156
|
+
|
|
157
|
+
args_h = case args
|
|
158
|
+
when Hash then symbolize(hash: args)
|
|
159
|
+
when String
|
|
160
|
+
s = args.strip
|
|
161
|
+
begin
|
|
162
|
+
parsed = JSON.parse(s, symbolize_names: true)
|
|
163
|
+
parsed.is_a?(Hash) ? parsed : { value: parsed }
|
|
164
|
+
rescue JSON::ParserError
|
|
165
|
+
h = {}
|
|
166
|
+
s.scan(/([A-Za-z_]\w*)\s*[:=]\s*(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^\s,)}{]+))/) do
|
|
167
|
+
k = Regexp.last_match(1)
|
|
168
|
+
h[k.to_sym] = Regexp.last_match(2) || Regexp.last_match(3) || Regexp.last_match(4)
|
|
169
|
+
end
|
|
170
|
+
if h.empty?
|
|
171
|
+
entry = (Registry.lookup(name: name) if defined?(Registry))
|
|
172
|
+
req = Array(entry&.schema&.dig(:parameters, :required))
|
|
173
|
+
h = req.length == 1 ? { req.first.to_sym => s } : { command: s }
|
|
174
|
+
end
|
|
175
|
+
h
|
|
176
|
+
end
|
|
177
|
+
else
|
|
178
|
+
{}
|
|
179
|
+
end
|
|
180
|
+
key = "#{name}|#{JSON.generate(args_h)}"
|
|
181
|
+
next if seen[key]
|
|
182
|
+
|
|
183
|
+
seen[key] = true
|
|
184
|
+
calls << {
|
|
185
|
+
id: "textcall_#{calls.length + 1}_#{SecureRandom.hex(3)}",
|
|
186
|
+
type: 'function',
|
|
187
|
+
function: {
|
|
188
|
+
name: name,
|
|
189
|
+
# OpenAI/xAI wire format requires a JSON string, not a map.
|
|
190
|
+
arguments: JSON.generate(args_h)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Balanced-delimiter extractor used for name(...) and call:name{...}.
|
|
196
|
+
extract_balanced = lambda do |open_ch, close_ch, from|
|
|
197
|
+
depth = 1
|
|
198
|
+
i = from
|
|
199
|
+
in_s = nil
|
|
200
|
+
esc = false
|
|
201
|
+
while i < text.length && depth.positive?
|
|
202
|
+
ch = text[i]
|
|
203
|
+
if in_s
|
|
204
|
+
if esc
|
|
205
|
+
esc = false
|
|
206
|
+
elsif ch == '\\'
|
|
207
|
+
esc = true
|
|
208
|
+
elsif ch == in_s
|
|
209
|
+
in_s = nil
|
|
210
|
+
end
|
|
211
|
+
elsif ['"', "'"].include?(ch)
|
|
212
|
+
in_s = ch
|
|
213
|
+
elsif ch == open_ch
|
|
214
|
+
depth += 1
|
|
215
|
+
elsif ch == close_ch
|
|
216
|
+
depth -= 1
|
|
217
|
+
end
|
|
218
|
+
i += 1
|
|
219
|
+
end
|
|
220
|
+
depth.zero? ? [text[from...(i - 1)].to_s.strip, i] : nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# JSON object forms:
|
|
224
|
+
# {"name":"shell","arguments":{...}}
|
|
225
|
+
# {"function":{"name":"shell","arguments":{...}}}
|
|
226
|
+
# {"tool":"shell","arguments":{...}} / {"call":"shell",...}
|
|
227
|
+
# {"type":"call","name":"shell",...}
|
|
228
|
+
text.scan(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/m).each do |blob|
|
|
229
|
+
begin
|
|
230
|
+
j = JSON.parse(blob, symbolize_names: true)
|
|
231
|
+
rescue JSON::ParserError
|
|
232
|
+
next
|
|
233
|
+
end
|
|
234
|
+
next unless j.is_a?(Hash)
|
|
235
|
+
|
|
236
|
+
name = (
|
|
237
|
+
j[:name] || j[:tool] || j[:call] ||
|
|
238
|
+
j.dig(:function, :name) || j.dig(:tool_call, :name)
|
|
239
|
+
).to_s
|
|
240
|
+
# Skip pure type tags mistaken as names (e.g. {"call":{...}} trees).
|
|
241
|
+
next if name.empty? || %w[function tool_call].include?(name)
|
|
242
|
+
|
|
243
|
+
args = j[:arguments] || j[:args] || j[:parameters] ||
|
|
244
|
+
j.dig(:function, :arguments) || j.dig(:tool_call, :arguments) || {}
|
|
245
|
+
add.call(name, args)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Colon-brace forms (OpenWebUI / abliterated dumps):
|
|
249
|
+
# call:shell{command: "uname -s"}
|
|
250
|
+
# tool:shell{"command":"id"}
|
|
251
|
+
# call:shell{command="id"}
|
|
252
|
+
rx_colon = /\b(?:call|tool)\s*:\s*(#{names_alt})\s*\{/i
|
|
253
|
+
idx = 0
|
|
254
|
+
while (m = text.match(rx_colon, idx))
|
|
255
|
+
name = m[1]
|
|
256
|
+
extracted = extract_balanced.call('{', '}', m.end(0))
|
|
257
|
+
if extracted
|
|
258
|
+
# Re-wrap: balanced extractor yields the interior only. Paren form
|
|
259
|
+
# shell({...}) keeps braces inside (...); brace form must restore
|
|
260
|
+
# them so JSON.parse / kwarg scan see a full object body.
|
|
261
|
+
add.call(name, "{#{extracted[0]}}")
|
|
262
|
+
end
|
|
263
|
+
idx = m.begin(0) + 1
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Call forms: shell(command="...") / shell({"command":"id"}) / shell("id")
|
|
267
|
+
rx = /\b(#{names_alt})\s*\(/i
|
|
268
|
+
idx = 0
|
|
269
|
+
while (m = text.match(rx, idx))
|
|
270
|
+
name = m[1]
|
|
271
|
+
extracted = extract_balanced.call('(', ')', m.end(0))
|
|
272
|
+
add.call(name, extracted[0]) if extracted
|
|
273
|
+
idx = m.begin(0) + 1
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
calls
|
|
277
|
+
rescue StandardError
|
|
278
|
+
[]
|
|
279
|
+
end
|
|
280
|
+
|
|
121
281
|
private_class_method def self.symbolize(opts = {})
|
|
122
282
|
hash = opts[:hash] ||= {}
|
|
123
283
|
hash.each_with_object({}) { |(k, v), m| m[k.to_sym] = v }
|
|
@@ -143,6 +303,7 @@ module PWN
|
|
|
143
303
|
)
|
|
144
304
|
|
|
145
305
|
PWN::AI::Agent::Dispatch.repair_name(name: 'run_shell') # => 'shell'
|
|
306
|
+
PWN::AI::Agent::Dispatch.tool_calls_from_text(text: 'shell(command="id")')
|
|
146
307
|
|
|
147
308
|
#{self}.authors
|
|
148
309
|
USAGE
|
|
@@ -1189,7 +1189,8 @@ module PWN
|
|
|
1189
1189
|
|
|
1190
1190
|
bin = m[:name].to_s.split('_').first
|
|
1191
1191
|
tc = Array(delta[:changed]).find { |c| c[:path].to_s.include?("toolchain.#{bin}") }
|
|
1192
|
-
|
|
1192
|
+
tc_map = snap[:toolchain].is_a?(Hash) ? snap[:toolchain] : {}
|
|
1193
|
+
miss = tc_map[bin.to_sym].to_s.empty?
|
|
1193
1194
|
next unless tc || miss
|
|
1194
1195
|
|
|
1195
1196
|
findings << { kind: :tool_env_mismatch, tool: m[:name], success_rate: m[:success_rate], evidence: tc || "binary '#{bin}' not found in PATH", advice: "Re-verify `which #{bin}` / reinstall before relying on #{m[:name]}." }
|
|
@@ -1206,7 +1207,7 @@ module PWN
|
|
|
1206
1207
|
end
|
|
1207
1208
|
|
|
1208
1209
|
# 3) intel observations matching installed components
|
|
1209
|
-
pkgs = snap[:toolchain]
|
|
1210
|
+
pkgs = snap[:toolchain].is_a?(Hash) ? snap[:toolchain] : {}
|
|
1210
1211
|
observations(category: 'intel', limit: 100).each do |ob|
|
|
1211
1212
|
blob = ob[:data].to_s.downcase
|
|
1212
1213
|
pkgs.each do |bin, ver|
|
|
@@ -1218,7 +1219,7 @@ module PWN
|
|
|
1218
1219
|
end
|
|
1219
1220
|
|
|
1220
1221
|
# 4) :rf observations vs missing SDR hardware / binaries
|
|
1221
|
-
rf = snap[:rf]
|
|
1222
|
+
rf = snap[:rf].is_a?(Hash) ? snap[:rf] : {}
|
|
1222
1223
|
hw_present = %i[rtl_sdr hackrf flipper gqrx_sock].any? { |k| rf_present?(val: rf[k]) }
|
|
1223
1224
|
observations(category: 'rf', limit: 50).each do |ob|
|
|
1224
1225
|
miss = RF_BINS.select { |b| pkgs[b.to_sym].to_s.empty? }
|
|
@@ -1240,7 +1241,10 @@ module PWN
|
|
|
1240
1241
|
Learning.outcomes(limit: 30, success: false).each do |o|
|
|
1241
1242
|
next unless o[:task].to_s.include?(host) || o[:details].to_s.include?(host)
|
|
1242
1243
|
|
|
1243
|
-
|
|
1244
|
+
dom_sha = case ob[:data]
|
|
1245
|
+
when Hash then ob[:data][:dom_sha] || ob[:data]['dom_sha']
|
|
1246
|
+
end
|
|
1247
|
+
findings << { kind: :target_web_drift, target: tgt, dom_sha: dom_sha, task: o[:task], advice: "Target #{host} DOM changed (#{ob[:timestamp]}) around this failure - re-recon before assuming your technique is wrong." }
|
|
1244
1248
|
end
|
|
1245
1249
|
end
|
|
1246
1250
|
end
|
|
@@ -1253,7 +1257,7 @@ module PWN
|
|
|
1253
1257
|
end
|
|
1254
1258
|
|
|
1255
1259
|
# 7) :intel observations whose source anchor is currently probe_web-unreachable -> downgrade
|
|
1256
|
-
web = snap[:web]
|
|
1260
|
+
web = snap[:web].is_a?(Hash) ? snap[:web] : {}
|
|
1257
1261
|
web.each do |host, fp|
|
|
1258
1262
|
next unless fp.is_a?(Hash) && (fp[:reachable] == false || fp[:status].to_i >= 500)
|
|
1259
1263
|
|
|
@@ -1088,7 +1088,8 @@ module PWN
|
|
|
1088
1088
|
# Mistakes(tool:'assumption') so KNOWN MISTAKES warns every future
|
|
1089
1089
|
# run off that specific hallucination.
|
|
1090
1090
|
private_class_method def self.fact_check_local_final(opts = {})
|
|
1091
|
-
|
|
1091
|
+
eng = defined?(PWN::Env) ? PWN::Env.dig(:ai, :active).to_s.downcase.to_sym : nil
|
|
1092
|
+
return unless %i[ollama openwebui].include?(eng)
|
|
1092
1093
|
return unless defined?(Extrospection) && Extrospection.respond_to?(:verify)
|
|
1093
1094
|
|
|
1094
1095
|
claims = opts[:final].to_s.scan(CLAIM_RX).flatten.compact.uniq
|