pwn 0.5.686 → 0.5.688
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/.gitignore +2 -0
- data/.ruby-version +1 -1
- data/Gemfile +3 -3
- data/README.md +3 -4
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Configuration.md +5 -4
- data/documentation/General-PWN-Usage.md +1 -1
- data/documentation/Home.md +1 -1
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Mistakes.md +1 -1
- data/documentation/Reinforcement-Learning.md +4 -6
- data/documentation/Troubleshooting.md +1 -1
- data/documentation/pwn-REPL.md +1 -1
- data/documentation/pwn-ai-Agent.md +2 -3
- data/lib/pwn/ai/agent/curriculum.rb +2 -2
- data/lib/pwn/ai/agent/dispatch.rb +1 -1
- data/lib/pwn/ai/agent/learning.rb +150 -32
- data/lib/pwn/ai/agent/loop.rb +291 -129
- data/lib/pwn/ai/agent/prompt_builder.rb +47 -29
- data/lib/pwn/ai/agent/registry.rb +2 -1
- data/lib/pwn/ai/agent/reward.rb +9 -0
- data/lib/pwn/ai/agent/swarm.rb +3 -1
- data/lib/pwn/ai/agent/task_summarizer.rb +23 -6
- data/lib/pwn/ai/agent/tool_guard.rb +109 -0
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +44 -20
- data/lib/pwn/ai/agent/tools/shell.rb +17 -4
- data/lib/pwn/ai/agent/tools/skills.rb +63 -2
- data/lib/pwn/ai/anthropic.rb +25 -5
- data/lib/pwn/ai/gemini.rb +25 -5
- data/lib/pwn/ai/grok.rb +30 -13
- data/lib/pwn/ai/http_retry.rb +78 -0
- data/lib/pwn/ai/ollama.rb +25 -5
- data/lib/pwn/ai/open_ai.rb +26 -13
- data/lib/pwn/ai/open_web_ui.rb +25 -5
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/config.rb +1 -1
- data/lib/pwn/plugins/log.rb +183 -41
- data/lib/pwn/plugins/repl.rb +50 -15
- data/lib/pwn/plugins/transparent_browser.rb +16 -2
- data/lib/pwn/sessions.rb +50 -1
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reinforced_feedback_loop_spec.rb +9 -11
- data/spec/lib/pwn/ai/agent/loop_spec.rb +180 -20
- data/spec/lib/pwn/ai/agent/prompt_builder_spec.rb +3 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/reward_spec.rb +13 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +6 -0
- data/spec/lib/pwn/ai/agent/task_summarizer_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +24 -0
- data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +10 -0
- data/spec/lib/pwn/ai/agent/tools/skills_spec.rb +29 -0
- data/spec/lib/pwn/ai/grok_spec.rb +22 -0
- data/spec/lib/pwn/ai/http_retry_spec.rb +55 -0
- data/spec/lib/pwn/plugins/log_spec.rb +170 -19
- data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +14 -4
- data/spec/lib/pwn/plugins/transparent_browser_spec.rb +8 -0
- data/spec/lib/pwn/sessions_spec.rb +24 -0
- data/third_party/pwn_rdoc.jsonl +41 -5
- data/tmp_pwn_critic_skills_flaw.txt +9 -0
- metadata +11 -8
data/lib/pwn/plugins/log.rb
CHANGED
|
@@ -13,6 +13,9 @@ module PWN
|
|
|
13
13
|
authors help to_s inspect class object_id hash eql? == equal? !
|
|
14
14
|
method public_send send __send__ instance_eval class_eval
|
|
15
15
|
public_class_method private_class_method
|
|
16
|
+
debug_on? debug_progress start_debug_session quiet_debug_tui!
|
|
17
|
+
loud_debug_tui! budget_scar? effective_count safe_check
|
|
18
|
+
rank_for_request usable_preference? cause_crumb
|
|
16
19
|
].freeze
|
|
17
20
|
TRACE_SKIP_PREFIXES = %w[
|
|
18
21
|
PWN::Plugins::Log
|
|
@@ -107,6 +110,7 @@ module PWN
|
|
|
107
110
|
|
|
108
111
|
if msg.instance_of?(Interrupt)
|
|
109
112
|
logger.level = Logger::WARN
|
|
113
|
+
note_interrupt!(where: 'CTRL+C', which_self: which_self) if debug_enabled?
|
|
110
114
|
if driver_name == 'pwn'
|
|
111
115
|
log_event += ' => CTRL+C Detected.'
|
|
112
116
|
else
|
|
@@ -124,6 +128,7 @@ module PWN
|
|
|
124
128
|
|
|
125
129
|
logger.add(logger.level, log_event, which_self)
|
|
126
130
|
rescue Interrupt
|
|
131
|
+
note_interrupt!(where: 'CTRL+C', which_self: self) if debug_enabled?
|
|
127
132
|
puts "\n#{self}.#{__method__} => Goodbye."
|
|
128
133
|
rescue StandardError => e
|
|
129
134
|
raise e
|
|
@@ -141,33 +146,69 @@ module PWN
|
|
|
141
146
|
@debug_path
|
|
142
147
|
end
|
|
143
148
|
|
|
144
|
-
# Open
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
# Open a debug session. Request traces go to
|
|
150
|
+
# /tmp/pwn-ai-DEBUG-<session_id>-RN.log via next_request_log!.
|
|
151
|
+
# opts[:path] is a test override that writes to one file.
|
|
148
152
|
public_class_method def self.start_debug(opts = {})
|
|
149
|
-
|
|
153
|
+
if debug_enabled? && opts[:path].to_s.empty?
|
|
154
|
+
@debug_tee = opts[:tee] if opts.key?(:tee)
|
|
155
|
+
sid = sanitize_debug_session_id(session_id: opts[:session_id])
|
|
156
|
+
@debug_session_id = sid unless sid.empty?
|
|
157
|
+
return @debug_path
|
|
158
|
+
end
|
|
150
159
|
|
|
151
|
-
|
|
160
|
+
stop_trace! if @debug_tp
|
|
152
161
|
path = opts[:path].to_s
|
|
153
|
-
path = "/tmp/pwn-ai-DEBUG-#{ts}.1.log" if path.empty?
|
|
154
|
-
stem, idx = debug_path_parts(path: path)
|
|
155
|
-
path = "#{stem}.#{idx}.log"
|
|
156
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
157
|
-
io = File.open(path, 'a')
|
|
158
|
-
io.sync = true
|
|
159
|
-
@debug_file = io
|
|
160
|
-
@debug_path = path
|
|
161
|
-
@debug_stem = stem
|
|
162
|
-
@debug_index = idx
|
|
163
162
|
@debug_tee = opts[:tee]
|
|
164
163
|
@debug_tui_quiet = false
|
|
165
164
|
@debug_enabled = true
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
@debug_session_id = sanitize_debug_session_id(session_id: opts[:session_id])
|
|
166
|
+
@debug_req_n = 0
|
|
167
|
+
@debug_request_open = false
|
|
168
|
+
if path.empty?
|
|
169
|
+
close_debug_file!
|
|
170
|
+
@debug_path = nil
|
|
171
|
+
else
|
|
172
|
+
open_debug_file!(path: path)
|
|
173
|
+
end
|
|
174
|
+
start_trace!(prefixes: opts[:prefixes]) if opts[:trace] == true
|
|
175
|
+
progress(msg: 'debug session start', which_self: self) if @debug_file
|
|
176
|
+
@debug_path
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
public_class_method def self.next_request_log!(opts = {})
|
|
180
|
+
return unless debug_enabled?
|
|
181
|
+
return @debug_path if @debug_request_open && opts[:force] != true
|
|
182
|
+
|
|
183
|
+
sid = sanitize_debug_session_id(session_id: opts[:session_id])
|
|
184
|
+
sid = @debug_session_id if sid.empty?
|
|
185
|
+
sid = 'nosession' if sid.empty?
|
|
186
|
+
@debug_session_id = sid
|
|
187
|
+
n = next_debug_request_n(session_id: sid)
|
|
188
|
+
path = "/tmp/pwn-ai-DEBUG-#{sid}-R#{n}.log"
|
|
189
|
+
open_debug_file!(path: path)
|
|
190
|
+
@debug_req_n = n
|
|
191
|
+
@debug_request_open = true
|
|
192
|
+
progress(msg: "request log R#{n} path=#{path}", which_self: self)
|
|
168
193
|
path
|
|
169
194
|
end
|
|
170
195
|
|
|
196
|
+
public_class_method def self.finish_request_log!(opts = {})
|
|
197
|
+
return unless debug_enabled?
|
|
198
|
+
return unless @debug_request_open || opts[:force] == true
|
|
199
|
+
|
|
200
|
+
bits = [
|
|
201
|
+
"footer iter=#{opts[:iter].to_i}",
|
|
202
|
+
"tools_called=#{opts[:tools_called].to_i}",
|
|
203
|
+
"engine_s=#{opts[:engine_s].to_f.round(3)}",
|
|
204
|
+
"final_chars=#{opts[:final_chars].to_i}"
|
|
205
|
+
]
|
|
206
|
+
bits << "nested=#{opts[:nested]}" unless opts[:nested].to_s.empty?
|
|
207
|
+
progress(msg: bits.join(' '), which_self: self)
|
|
208
|
+
@debug_request_open = false
|
|
209
|
+
@debug_path
|
|
210
|
+
end
|
|
211
|
+
|
|
171
212
|
public_class_method def self.stop_debug(opts = {})
|
|
172
213
|
reason = opts[:reason].to_s if opts.is_a?(Hash)
|
|
173
214
|
if debug_enabled?
|
|
@@ -175,37 +216,49 @@ module PWN
|
|
|
175
216
|
progress(msg: tail, which_self: self)
|
|
176
217
|
end
|
|
177
218
|
stop_trace!
|
|
178
|
-
begin
|
|
179
|
-
@debug_file&.close unless @debug_file.nil? || @debug_file.closed?
|
|
180
|
-
rescue StandardError
|
|
181
|
-
nil
|
|
182
|
-
end
|
|
183
219
|
path = @debug_path
|
|
184
|
-
|
|
220
|
+
close_debug_file!
|
|
185
221
|
@debug_path = nil
|
|
186
|
-
@
|
|
187
|
-
@
|
|
222
|
+
@debug_session_id = nil
|
|
223
|
+
@debug_req_n = nil
|
|
224
|
+
@debug_request_open = false
|
|
188
225
|
@debug_tee = nil
|
|
189
226
|
@debug_tui_quiet = false
|
|
190
227
|
@debug_enabled = false
|
|
191
228
|
path
|
|
192
229
|
end
|
|
193
230
|
|
|
194
|
-
private_class_method def self.
|
|
195
|
-
|
|
196
|
-
|
|
231
|
+
private_class_method def self.sanitize_debug_session_id(opts = {})
|
|
232
|
+
opts[:session_id].to_s.gsub(/[^A-Za-z0-9._-]/, '_')[0, 80]
|
|
233
|
+
end
|
|
197
234
|
|
|
198
|
-
|
|
235
|
+
private_class_method def self.next_debug_request_n(opts = {})
|
|
236
|
+
sid = sanitize_debug_session_id(session_id: opts[:session_id])
|
|
237
|
+
max_n = 0
|
|
238
|
+
Dir.glob("/tmp/pwn-ai-DEBUG-#{sid}-R*.log").each do |p|
|
|
239
|
+
n = p[/-R(\d+)\.log\z/, 1].to_i
|
|
240
|
+
max_n = n if n > max_n
|
|
241
|
+
end
|
|
242
|
+
max_n + 1
|
|
199
243
|
end
|
|
200
244
|
|
|
201
|
-
private_class_method def self.
|
|
245
|
+
private_class_method def self.close_debug_file!(opts = {})
|
|
202
246
|
return if opts[:skip]
|
|
203
|
-
return if @debug_file.nil? || @debug_file.closed?
|
|
204
|
-
return if @debug_file.size < DEBUG_LOG_MAX
|
|
205
247
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
248
|
+
begin
|
|
249
|
+
@debug_file&.close unless @debug_file.nil? || @debug_file.closed?
|
|
250
|
+
rescue StandardError
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
@debug_file = nil
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private_class_method def self.open_debug_file!(opts = {})
|
|
257
|
+
path = opts[:path].to_s
|
|
258
|
+
return if path.empty?
|
|
259
|
+
|
|
260
|
+
close_debug_file!
|
|
261
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
209
262
|
io = File.open(path, 'a')
|
|
210
263
|
io.sync = true
|
|
211
264
|
@debug_file = io
|
|
@@ -233,11 +286,15 @@ module PWN
|
|
|
233
286
|
Thread.current[:pwn_log_progress] = true
|
|
234
287
|
msg = sanitize_debug_text(text: opts[:msg].to_s)
|
|
235
288
|
which = opts[:which_self] || self
|
|
236
|
-
line = format_progress(
|
|
289
|
+
line = format_progress(
|
|
290
|
+
msg: msg,
|
|
291
|
+
which_self: which,
|
|
292
|
+
keep_newlines: opts[:keep_newlines],
|
|
293
|
+
cap: opts[:cap]
|
|
294
|
+
)
|
|
237
295
|
begin
|
|
238
296
|
@debug_file&.puts(line)
|
|
239
297
|
@debug_file&.flush
|
|
240
|
-
roll_debug_log!
|
|
241
298
|
rescue StandardError
|
|
242
299
|
nil
|
|
243
300
|
end
|
|
@@ -245,6 +302,8 @@ module PWN
|
|
|
245
302
|
if !@debug_tui_quiet && tee.respond_to?(:puts)
|
|
246
303
|
begin
|
|
247
304
|
tee.puts(color_progress(line: line))
|
|
305
|
+
tee.flush if tee.respond_to?(:flush)
|
|
306
|
+
$stdout.flush if $stdout.respond_to?(:flush)
|
|
248
307
|
rescue StandardError
|
|
249
308
|
nil
|
|
250
309
|
end
|
|
@@ -254,8 +313,88 @@ module PWN
|
|
|
254
313
|
Thread.current[:pwn_log_progress] = false
|
|
255
314
|
end
|
|
256
315
|
|
|
316
|
+
public_class_method def self.note_interrupt!(opts = {})
|
|
317
|
+
return false unless debug_enabled?
|
|
318
|
+
|
|
319
|
+
where = opts[:where].to_s
|
|
320
|
+
where = 'CTRL+C' if where.empty?
|
|
321
|
+
# CTRL+C can land mid-progress (HTTP spinner tee, tool row mirror).
|
|
322
|
+
# Clear the reentrancy latch so the Interrupt stamp always lands in the
|
|
323
|
+
# open RN file before the ensure footer / process unwind.
|
|
324
|
+
Thread.current[:pwn_log_progress] = false
|
|
325
|
+
at = Time.now
|
|
326
|
+
local_ts = at.strftime('%Y-%m-%d %H:%M:%S.%L%z')
|
|
327
|
+
msg = "Interrupt #{where} at=#{at.utc.iso8601(3)}"
|
|
328
|
+
which = opts[:which_self] || self
|
|
329
|
+
# Prefer the normal DEBUG stamp path; fall back to a direct file write
|
|
330
|
+
# if progress is still unavailable for any reason.
|
|
331
|
+
ok = progress(msg: msg, which_self: which)
|
|
332
|
+
unless ok
|
|
333
|
+
begin
|
|
334
|
+
who = which.is_a?(Module) ? (which.name || which.to_s) : which.to_s
|
|
335
|
+
line = "[DEBUG #{local_ts}] #{who} #{msg}".strip
|
|
336
|
+
@debug_file&.puts(line)
|
|
337
|
+
@debug_file&.flush
|
|
338
|
+
ok = true
|
|
339
|
+
rescue StandardError
|
|
340
|
+
ok = false
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
# Also clone the operator-facing TUI shape into the RN file.
|
|
344
|
+
mirror_tui!(msg: "[ #{local_ts} → pwn-ai → Interrupt ] #{where}")
|
|
345
|
+
ok
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
public_class_method def self.note_exception!(opts = {})
|
|
349
|
+
return false unless debug_enabled?
|
|
350
|
+
|
|
351
|
+
err = opts[:error]
|
|
352
|
+
return false unless err.respond_to?(:message)
|
|
353
|
+
|
|
354
|
+
where = opts[:where].to_s
|
|
355
|
+
where = 'Loop.run' if where.empty?
|
|
356
|
+
bt = Array(err.backtrace).join("\n")
|
|
357
|
+
progress(
|
|
358
|
+
msg: "exception #{where} #{err.class}: #{err.message}\n#{bt}",
|
|
359
|
+
which_self: opts[:which_self] || self,
|
|
360
|
+
keep_newlines: true,
|
|
361
|
+
cap: 0
|
|
362
|
+
)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Clone operator-visible TUI rows into the open RN request log (no ANSI).
|
|
366
|
+
# Used for [ ts → pwn-ai → tool ] / → result / task briefs so the file
|
|
367
|
+
# matches what the operator saw without needing a TracePoint dump.
|
|
368
|
+
public_class_method def self.mirror_tui!(opts = {})
|
|
369
|
+
return unless debug_enabled?
|
|
370
|
+
return unless opts.is_a?(Hash)
|
|
371
|
+
|
|
372
|
+
text = opts[:msg].to_s
|
|
373
|
+
text = text.gsub(/\e\[[0-9;]*m/, '')
|
|
374
|
+
text = sanitize_debug_text(text: text)
|
|
375
|
+
return if text.strip.empty?
|
|
376
|
+
|
|
377
|
+
begin
|
|
378
|
+
io = @debug_file
|
|
379
|
+
return if io.nil? || (io.respond_to?(:closed?) && io.closed?)
|
|
380
|
+
|
|
381
|
+
io.write(text.end_with?("\n") ? text : "#{text}\n")
|
|
382
|
+
io.flush
|
|
383
|
+
rescue StandardError
|
|
384
|
+
return
|
|
385
|
+
end
|
|
386
|
+
text
|
|
387
|
+
end
|
|
388
|
+
|
|
257
389
|
private_class_method def self.format_progress(opts = {})
|
|
258
|
-
|
|
390
|
+
cap = if opts.key?(:cap)
|
|
391
|
+
opts[:cap].to_i
|
|
392
|
+
else
|
|
393
|
+
4_000
|
|
394
|
+
end
|
|
395
|
+
msg = opts[:msg].to_s
|
|
396
|
+
msg = msg.tr("\n", ' ') unless opts[:keep_newlines]
|
|
397
|
+
msg = msg[0, cap] if cap.positive? && msg.length > cap
|
|
259
398
|
which = opts[:which_self]
|
|
260
399
|
who = if which.is_a?(Module)
|
|
261
400
|
(which.name || which.to_s).to_s
|
|
@@ -271,7 +410,7 @@ module PWN
|
|
|
271
410
|
|
|
272
411
|
private_class_method def self.color_progress(opts = {})
|
|
273
412
|
line = opts[:line].to_s
|
|
274
|
-
"\
|
|
413
|
+
"\e[35m#{line}\e[0m"
|
|
275
414
|
end
|
|
276
415
|
|
|
277
416
|
private_class_method def self.trace_module_name(opts = {})
|
|
@@ -414,10 +553,13 @@ module PWN
|
|
|
414
553
|
|
|
415
554
|
path = #{self}.start_debug(
|
|
416
555
|
tee: $stdout,
|
|
417
|
-
|
|
556
|
+
session_id: 'optional - pwn-ai session id'
|
|
418
557
|
)
|
|
558
|
+
#{self}.next_request_log!(session_id: 'optional')
|
|
419
559
|
#{self}.progress(msg: 'stage', which_self: self)
|
|
560
|
+
#{self}.finish_request_log!(iter: 1, tools_called: 0, engine_s: 0.2, final_chars: 12)
|
|
420
561
|
#{self}.stop_debug
|
|
562
|
+
# start_debug(trace: true) enables per-call TracePoint (opt-in)
|
|
421
563
|
"
|
|
422
564
|
end
|
|
423
565
|
end
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -471,15 +471,14 @@ module PWN
|
|
|
471
471
|
PWN::Config.load_memory
|
|
472
472
|
mem_count = (PWN.const_defined?(:Memory) ? PWN::Memory.load.keys.length : 0)
|
|
473
473
|
sess = begin
|
|
474
|
-
PWN::Sessions.create(title: "pwn-ai #{Time.now.strftime('%Y-%m-%d %H:%M')}", source: 'pwn-ai
|
|
474
|
+
PWN::Sessions.create(title: "pwn-ai #{Time.now.strftime('%Y-%m-%d %H:%M')}", source: 'pwn-ai')
|
|
475
475
|
rescue StandardError
|
|
476
476
|
nil
|
|
477
477
|
end
|
|
478
478
|
pi.config.pwn_ai_session_id = sess[:id] if sess
|
|
479
479
|
cron_count = (PWN.const_defined?(:Cron) ? PWN::Cron.list.keys.length : 0)
|
|
480
480
|
|
|
481
|
-
puts
|
|
482
|
-
[*] pwn-ai agent TUI activated (PWN REPL driver w/ memory, sessions, delegation, cron)."
|
|
481
|
+
puts '[*] pwn-ai agent TUI activated (PWN REPL driver w/ memory, sessions, delegation, cron).'
|
|
483
482
|
puts "[*] Memory facts: #{mem_count} | Session: #{pi.config.pwn_ai_session_id} | Cron jobs: #{cron_count} | Skills: #{skills_count}"
|
|
484
483
|
puts '[*] Instruct the AI agent to carry out a task, e.g.:'
|
|
485
484
|
puts " 'Use NmapIt to port scan target.com then use TransparentBrowser to spider and SAST::TestCaseEngine to analyze code if cloned. Generate report with PWN::Reports.'"
|
|
@@ -488,6 +487,8 @@ module PWN
|
|
|
488
487
|
puts "[*] Type 'back' to exit pwn-ai mode."
|
|
489
488
|
puts '[*] MULTILINE in pwn-ai: SHIFT+ENTER (or ALT+ENTER, or trailing `\\`) inserts a newline; ENTER submits to the AI.'
|
|
490
489
|
puts "[*] tmux + terminator users: Ensure ~/.tmux.conf has 'set -s extended-keys on' and 'set -g xterm-keys on', then restart tmux. Use TERM=xterm-256color."
|
|
490
|
+
tag = pi.config.pwn_ai_session_id.to_s.empty? ? '<SESSION_ID>' : pi.config.pwn_ai_session_id
|
|
491
|
+
puts "\n\n\npwn-ai debug ON → /tmp/pwn-ai-DEBUG-#{tag}-R<REQUEST_NUMBER>.log"
|
|
491
492
|
end
|
|
492
493
|
end
|
|
493
494
|
|
|
@@ -1043,8 +1044,8 @@ module PWN
|
|
|
1043
1044
|
end
|
|
1044
1045
|
end
|
|
1045
1046
|
|
|
1046
|
-
Pry::Commands.create_command 'toggle-
|
|
1047
|
-
description 'Stream pwn-ai
|
|
1047
|
+
Pry::Commands.create_command 'toggle-debug' do
|
|
1048
|
+
description 'Stream pwn-ai stage log to the TUI and /tmp/pwn-ai-DEBUG-<SESSION_ID>-RN.log (trace: Env ai.agent.debug_trace)'
|
|
1048
1049
|
|
|
1049
1050
|
def process
|
|
1050
1051
|
pi = pry_instance
|
|
@@ -1057,9 +1058,10 @@ module PWN
|
|
|
1057
1058
|
output.puts 'pwn-ai debug OFF'
|
|
1058
1059
|
end
|
|
1059
1060
|
else
|
|
1060
|
-
|
|
1061
|
+
sid = pi.config.pwn_ai_session_id
|
|
1062
|
+
PWN::Plugins::Log.start_debug(tee: output, session_id: sid)
|
|
1061
1063
|
pi.config.pwn_ai_debug = true
|
|
1062
|
-
output.puts
|
|
1064
|
+
output.puts 'pwn-ai debug ON.'
|
|
1063
1065
|
end
|
|
1064
1066
|
end
|
|
1065
1067
|
end
|
|
@@ -1205,22 +1207,49 @@ module PWN
|
|
|
1205
1207
|
# belong only to the subsequent per-tool lines (one-to-many).
|
|
1206
1208
|
on_tool = lambda do |name, args, result|
|
|
1207
1209
|
# Task summaries are shown in their entirety (multi-line OK).
|
|
1208
|
-
#
|
|
1210
|
+
# Tool request + result are shown in full (no char cap).
|
|
1211
|
+
# Raw ANSI only — PS1 SOH/STX on live stdout swallows later rows.
|
|
1212
|
+
# When debug is on, the same plain text is mirrored into the
|
|
1213
|
+
# open /tmp pwn-ai-DEBUG-…-RN.log for human troubleshooting.
|
|
1214
|
+
mirror = lambda do |plain|
|
|
1215
|
+
next unless pi.config.pwn_ai_debug && defined?(PWN::Plugins::Log)
|
|
1216
|
+
|
|
1217
|
+
PWN::Plugins::Log.mirror_tui!(msg: plain)
|
|
1218
|
+
end
|
|
1209
1219
|
if name.to_s == 'task'
|
|
1210
1220
|
body = args.is_a?(String) ? args.to_s : args.inspect
|
|
1211
1221
|
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1222
|
+
header = "[ #{timestamp} → pwn-ai → task ]"
|
|
1223
|
+
print "\e[33m#{header}\e[0m "
|
|
1224
|
+
body_out = +''
|
|
1225
|
+
body.to_s.each_line do |ln|
|
|
1226
|
+
puts "\e[32m #{ln.rstrip}\e[0m"
|
|
1227
|
+
body_out << " #{ln.rstrip}\n"
|
|
1228
|
+
end
|
|
1229
|
+
mirror.call("#{header}\n#{body_out}")
|
|
1215
1230
|
next
|
|
1216
1231
|
end
|
|
1217
1232
|
|
|
1218
|
-
|
|
1233
|
+
argv = args.is_a?(String) ? args.to_s : args.inspect
|
|
1219
1234
|
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
1220
|
-
|
|
1235
|
+
header = "[ #{timestamp} → pwn-ai → #{name} ]"
|
|
1236
|
+
puts "\e[33m#{header}\e[0m"
|
|
1237
|
+
argv_out = +''
|
|
1238
|
+
argv.to_s.each_line do |ln|
|
|
1239
|
+
puts "\e[33m #{ln.rstrip}\e[0m"
|
|
1240
|
+
argv_out << " #{ln.rstrip}\n"
|
|
1241
|
+
end
|
|
1221
1242
|
|
|
1222
1243
|
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
1223
|
-
|
|
1244
|
+
res_header = "#{timestamp} → result"
|
|
1245
|
+
puts "\e[36m#{res_header}\e[0m"
|
|
1246
|
+
res_out = +''
|
|
1247
|
+
result.to_s.each_line do |ln|
|
|
1248
|
+
puts "\e[36m #{ln.rstrip}\e[0m"
|
|
1249
|
+
res_out << " #{ln.rstrip}\n"
|
|
1250
|
+
end
|
|
1251
|
+
puts
|
|
1252
|
+
mirror.call("#{header}\n#{argv_out}#{res_header}\n#{res_out}\n")
|
|
1224
1253
|
end
|
|
1225
1254
|
final = PWN::AI::Agent::Loop.run(
|
|
1226
1255
|
request: orig_request,
|
|
@@ -1231,8 +1260,9 @@ module PWN
|
|
|
1231
1260
|
debug_tee: $stdout
|
|
1232
1261
|
)
|
|
1233
1262
|
$stdout.flush
|
|
1234
|
-
puts "\n\
|
|
1263
|
+
puts "\n\e[32m#{final}\e[0m\n\n"
|
|
1235
1264
|
$stdout.flush
|
|
1265
|
+
PWN::Plugins::Log.mirror_tui!(msg: "\n#{final}\n\n") if pi.config.pwn_ai_debug && defined?(PWN::Plugins::Log)
|
|
1236
1266
|
if pi.config.pwn_ai_debug && sess_id && PWN.const_defined?(:Sessions)
|
|
1237
1267
|
PWN::Plugins::Log.progress(
|
|
1238
1268
|
msg: "session=#{sess_id}",
|
|
@@ -1241,7 +1271,12 @@ module PWN
|
|
|
1241
1271
|
end
|
|
1242
1272
|
request.replace('nil')
|
|
1243
1273
|
next
|
|
1274
|
+
rescue Interrupt
|
|
1275
|
+
Thread.current[:pwn_log_progress] = false
|
|
1276
|
+
PWN::Plugins::Log.note_interrupt!(where: 'CTRL+C', which_self: PWN::Plugins::REPL) if pi.config.pwn_ai_debug && defined?(PWN::Plugins::Log)
|
|
1277
|
+
raise
|
|
1244
1278
|
rescue StandardError => e
|
|
1279
|
+
PWN::Plugins::Log.note_exception!(error: e, where: 'native agent loop', which_self: PWN::Plugins::REPL) if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.respond_to?(:note_exception!)
|
|
1245
1280
|
warn "[pwn-ai] native agent loop failed (#{e.class}: #{e.message}\n#{e.backtrace}); " \
|
|
1246
1281
|
'falling back to legacy regex-ReAct.'
|
|
1247
1282
|
ensure
|
|
@@ -41,6 +41,17 @@ module PWN
|
|
|
41
41
|
raise e
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
private_class_method def self.apply_driver_timeouts!(opts = {})
|
|
45
|
+
browser = opts[:browser_obj].is_a?(Hash) ? opts[:browser_obj][:browser] : nil
|
|
46
|
+
return unless browser.respond_to?(:driver)
|
|
47
|
+
|
|
48
|
+
timeouts = browser.driver.manage.timeouts
|
|
49
|
+
timeouts.page_load = 45
|
|
50
|
+
timeouts.script = 30
|
|
51
|
+
rescue StandardError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
44
55
|
# Supported Method Parameters::
|
|
45
56
|
# browser_obj1 = PWN::Plugins::TransparentBrowser.open(
|
|
46
57
|
# browser_type: 'optional - :firefox|:chrome|:headless|:rest|:websocket (defaults to :chrome)',
|
|
@@ -66,8 +77,10 @@ module PWN
|
|
|
66
77
|
devtools = opts[:devtools] ||= false
|
|
67
78
|
devtools = true if devtools_supported.include?(browser_type) && devtools
|
|
68
79
|
|
|
69
|
-
#
|
|
70
|
-
|
|
80
|
+
# Element waits (Watir) vs navigation (Selenium page_load) are
|
|
81
|
+
# separate. 900s element waits made Wix browser.links.map hang for
|
|
82
|
+
# hours. Keep navigation bounded too.
|
|
83
|
+
Watir.default_timeout = 15 unless %i[rest websocket].include?(browser_type)
|
|
71
84
|
|
|
72
85
|
args = []
|
|
73
86
|
# args.push('--start-maximized')
|
|
@@ -365,6 +378,7 @@ module PWN
|
|
|
365
378
|
browser_obj[:devtools] = browser_obj[:browser].driver.bidi if firefox_types.include?(browser_type)
|
|
366
379
|
end
|
|
367
380
|
|
|
381
|
+
apply_driver_timeouts!(browser_obj: browser_obj)
|
|
368
382
|
browser_obj
|
|
369
383
|
rescue StandardError => e
|
|
370
384
|
puts e.backtrace
|
data/lib/pwn/sessions.rb
CHANGED
|
@@ -48,7 +48,7 @@ module PWN
|
|
|
48
48
|
# Supported Method Parameters::
|
|
49
49
|
# session = PWN::Sessions.create(
|
|
50
50
|
# title: 'optional - human title',
|
|
51
|
-
# source: 'optional - e.g. pwn-ai
|
|
51
|
+
# source: 'optional - e.g. pwn-ai',
|
|
52
52
|
# id: 'optional - fixed session id (tests / replay); default timestamp_hex'
|
|
53
53
|
# )
|
|
54
54
|
public_class_method def self.create(opts = {})
|
|
@@ -220,6 +220,54 @@ module PWN
|
|
|
220
220
|
}
|
|
221
221
|
end
|
|
222
222
|
|
|
223
|
+
# Current-session conversation for the next LLM call. User + assistant
|
|
224
|
+
# only (no tool/observation). Always on — not gated by recall wording.
|
|
225
|
+
# Tail-window by max_chars so a long JSONL cannot blow the context.
|
|
226
|
+
#
|
|
227
|
+
# msgs = PWN::Sessions.to_llm_messages(
|
|
228
|
+
# session_id: 'required',
|
|
229
|
+
# max_chars: 'optional - keep newest turns under this (default 48_000)',
|
|
230
|
+
# skip_request: 'optional - drop a trailing user line equal to this'
|
|
231
|
+
# )
|
|
232
|
+
public_class_method def self.to_llm_messages(opts = {})
|
|
233
|
+
sid = opts[:session_id]
|
|
234
|
+
return [] if sid.to_s.empty?
|
|
235
|
+
|
|
236
|
+
max_chars = opts[:max_chars].to_i
|
|
237
|
+
max_chars = 48_000 if max_chars <= 0
|
|
238
|
+
skip = opts[:skip_request].to_s
|
|
239
|
+
rows = load(session_id: sid).select do |row|
|
|
240
|
+
next false unless row.is_a?(Hash)
|
|
241
|
+
|
|
242
|
+
role = row[:role].to_s
|
|
243
|
+
next false unless %w[user assistant].include?(role)
|
|
244
|
+
|
|
245
|
+
body = row[:content].to_s
|
|
246
|
+
next false if body.strip.empty?
|
|
247
|
+
|
|
248
|
+
true
|
|
249
|
+
end
|
|
250
|
+
if !skip.empty? && rows.last && rows.last[:role].to_s == 'user' && rows.last[:content].to_s == skip
|
|
251
|
+
rows = rows[0...-1]
|
|
252
|
+
elsif !skip.empty?
|
|
253
|
+
rows = rows.reject { |row| row[:role].to_s == 'user' && row[:content].to_s == skip }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
picked = []
|
|
257
|
+
used = 0
|
|
258
|
+
rows.reverse_each do |row|
|
|
259
|
+
body = row[:content].to_s
|
|
260
|
+
n = body.length
|
|
261
|
+
break if picked.any? && (used + n) > max_chars
|
|
262
|
+
|
|
263
|
+
picked.unshift(role: row[:role].to_s, content: body)
|
|
264
|
+
used += n
|
|
265
|
+
end
|
|
266
|
+
picked
|
|
267
|
+
rescue StandardError
|
|
268
|
+
[]
|
|
269
|
+
end
|
|
270
|
+
|
|
223
271
|
# Supported Method Parameters::
|
|
224
272
|
# PWN::Sessions.delete(
|
|
225
273
|
# session_id: 'required',
|
|
@@ -497,6 +545,7 @@ module PWN
|
|
|
497
545
|
PWN::Sessions.append(session_id: sess[:id], role: 'user', content: 'Run NmapIt...')
|
|
498
546
|
transcript = PWN::Sessions.load(session_id: sess[:id])
|
|
499
547
|
hist = PWN::Sessions.to_response_history(session_id: sess[:id])
|
|
548
|
+
msgs = PWN::Sessions.to_llm_messages(session_id: sess[:id])
|
|
500
549
|
PWN::Sessions.recall(query: 'hping3', exclude_session_id: sess[:id])
|
|
501
550
|
PWN::Sessions.previous_id(exclude_session_id: sess[:id])
|
|
502
551
|
PWN::Sessions.list
|
data/lib/pwn/version.rb
CHANGED
|
@@ -423,6 +423,8 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
423
423
|
|
|
424
424
|
hint = curriculum.red_team_plan(request: 'goal', plan: "1. shell nmap\n2. shell msfconsole")
|
|
425
425
|
expect(hint).to start_with('[pwn-ai/red_team]')
|
|
426
|
+
src = File.read(curriculum.method(:red_team_plan).source_location.first)
|
|
427
|
+
expect(src).to match(/text_only:\s*true/)
|
|
426
428
|
end
|
|
427
429
|
end
|
|
428
430
|
|
|
@@ -1478,23 +1480,19 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
1478
1480
|
expect(src).not_to match(/n = \[n, hot_cap\]\.min/)
|
|
1479
1481
|
end
|
|
1480
1482
|
|
|
1481
|
-
it '
|
|
1483
|
+
it 'never strips tools because a round cap is about to fire' do
|
|
1482
1484
|
src = File.read(loop_mod.method(:run).source_location.first)
|
|
1483
|
-
expect(src).
|
|
1484
|
-
expect(src).
|
|
1485
|
-
# CF gate requires !budget_exhaustion_hot?
|
|
1485
|
+
expect(src).not_to match(/FINAL ITERATION/)
|
|
1486
|
+
expect(src).not_to match(/last_iter \? nil : tools/)
|
|
1486
1487
|
expect(src).to match(/!budget_exhaustion_hot\?/)
|
|
1487
1488
|
expect(src).to match(/Curriculum\.counterfactual/)
|
|
1488
1489
|
end
|
|
1489
1490
|
|
|
1490
|
-
it '
|
|
1491
|
+
it 'does not return a budget-exhausted canned final' do
|
|
1491
1492
|
src = File.read(loop_mod.method(:run).source_location.first)
|
|
1492
|
-
|
|
1493
|
-
expect(
|
|
1494
|
-
|
|
1495
|
-
expect(tail).to match(/append_session/)
|
|
1496
|
-
expect(tail).to match(/auto_introspect/)
|
|
1497
|
-
expect(tail).to match(/final_msg/)
|
|
1493
|
+
expect(src).not_to match(/shape: :budget_exhausted/)
|
|
1494
|
+
expect(src).not_to match(/iteration budget exhausted/)
|
|
1495
|
+
expect(src).to match(/may_finalize\?/)
|
|
1498
1496
|
end
|
|
1499
1497
|
|
|
1500
1498
|
it 'STATUS table lists Budget exhaust deepen' do
|