pwn 0.5.666 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad38a3062af2dfd655e372b35546535f8f652b39a8b91269ce8ee9e45a01ba9b
4
- data.tar.gz: c66ed8a1aa104008e480f5118b892a5132ce8047e0029ab4728381f8a4e00ba7
3
+ metadata.gz: 21fe6bb04741fbc78392d434588c1c86d0f605db0988bfa2192f68e9baa870ec
4
+ data.tar.gz: cf66359fb95bb12f08705d11dd304625878115902133d9e1ad0f8becfa8fb83a
5
5
  SHA512:
6
- metadata.gz: f27ba8cff4c35f7cb330ce1b1fad3cf7806759ff84a6706b61e6dd55864a3241c29990e151e63ae9218a065748e9383698dfec586245573c3d80a55a4bd9300a
7
- data.tar.gz: 2643e1501646d411888403352dd2e822f597f2222089a8d06ac1fac8d9b5c474d72a55ad072e150e102366e63bdb9d157a0ad1d2a2c935722b1ba80d20782788
6
+ metadata.gz: 35f079f6d7c765c6395f490f07c06ce3494b81487bba31c978367fc2b8a23a421a6b1d9a68cda02592fd94868ca343a13c441323b6e1dee0656f46ade8c88aef
7
+ data.tar.gz: b9a45751eaf30849ac1c98bd8d603df89d8522fb950dfc830f188769b0fc8e7b69848ed1f71053f7053da24a85e0c2f56f0f9a2c26b95de2040835a7e9871c47
data/.rubocop_todo.yml CHANGED
@@ -73,6 +73,7 @@ Naming/AccessorMethodName:
73
73
  - 'lib/pwn/ai/grok.rb'
74
74
  - 'lib/pwn/ai/ollama.rb'
75
75
  - 'lib/pwn/ai/open_ai.rb'
76
+ - 'lib/pwn/ai/open_web_ui.rb'
76
77
  - 'lib/pwn/blockchain/btc.rb'
77
78
  - 'lib/pwn/plugins/jira_data_center.rb'
78
79
  - 'lib/pwn/plugins/vin.rb'
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.15.1'
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.150.0'
93
- gem 'selenium-webdriver', '4.46.0'
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.5'
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'
data/README.md CHANGED
@@ -58,11 +58,20 @@ pwn.ai:grok/grok-4.5/0:500K[CURRENT_VERSION]:002 >>> Briefly describe your take
58
58
 
59
59
  **My take, in plain English:**
60
60
 
61
- pwn-ai’s reinforcement loop is mostly **learning in context**, not only weight training.
61
+ pwn-ai’s reinforced learning loop is less a classic offline RL trainer and more a living, multi-store control system wrapped around an agent turn. My take is that it is one of the more complete in-context self-improvement stacks I’ve seen in an agent harness, and also that its real power and its main failure modes come from the same place: it learns loudly in the prompt long before it learns quietly in weights.
62
62
 
63
- On every turn the agent acts with tools. Each result is scored in a few ways: quick “did this tool call really fail?” checks, longer-term tool success stats, and—when a final answer lands—a judge that scores whether the *goal* was actually met, plus per-step credit for which tool calls helped or hurt. Failures become fingerprints (mistakes) with fixes; wins become outcomes, exemplars, memory lessons, and sometimes skills. All of that is written back into the next prompt as “what worked,” “what not to repeat,” and “which tools pay off,” so behavior improves on the following run without waiting for a model train.
63
+ ## Bottom line
64
64
 
65
- Around that core sit self-checks: a sentinel that distrusts inflated success rates when the judge disagrees, night jobs that practice the worst repeating mistakes, and optional export of preference pairs for a gated LoRA update when a trainer and GPU exist. So the loop is: **act measure honestly remember (and fix) bias the next act**—with real weight updates as an optional outer ring, not the main daily path.
65
+ My take: pwn-ai implements a **closed-loop, dual-horizon, self-and-world-aware reinforcement system** around an LLM agent. The clever part is not that it has a reward model. The clever part is that it treats agent work as an ongoing control problem with:
66
+
67
+ - fast aversive conditioning (Mistakes),
68
+ - value estimates for actions (Metrics),
69
+ - episode scoring and replay (Learning/Reward),
70
+ - deliberate practice (Curriculum),
71
+ - and an external reality check (Extrospection),
72
+ - with a slower supervised/DPO hatch only when the diet and gates look sane.
73
+
74
+ It feels less like “fine-tune the model forever” and more like giving the agent a nervous system: pain, habit, memory, practice, and a rudimentary sense of whether the world changed. That is why it can improve overnight on a host with no trainer. It is also why health has to be measured by judge gap, repeating-mistake trend, trajectory fraction, and resolved scars rather than by tool success_rate alone.
66
75
  ```
67
76
 
68
77
  Offensive work is hard because the *tools* do not fit together. PWN's fix is
@@ -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
- miss = snap.dig(:toolchain, bin.to_sym).to_s.empty?
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
- findings << { kind: :target_web_drift, target: tgt, dom_sha: ob.dig(:data, :dom_sha), task: o[:task], advice: "Target #{host} DOM changed (#{ob[:timestamp]}) around this failure - re-recon before assuming your technique is wrong." }
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
- return unless defined?(PWN::Env) && PWN::Env.dig(:ai, :active).to_s.downcase.to_sym == :ollama
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