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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile +3 -3
  5. data/README.md +3 -4
  6. data/documentation/Agent-Tool-Registry.md +2 -2
  7. data/documentation/Configuration.md +5 -4
  8. data/documentation/General-PWN-Usage.md +1 -1
  9. data/documentation/Home.md +1 -1
  10. data/documentation/How-PWN-Works.md +1 -1
  11. data/documentation/Mistakes.md +1 -1
  12. data/documentation/Reinforcement-Learning.md +4 -6
  13. data/documentation/Troubleshooting.md +1 -1
  14. data/documentation/pwn-REPL.md +1 -1
  15. data/documentation/pwn-ai-Agent.md +2 -3
  16. data/lib/pwn/ai/agent/curriculum.rb +2 -2
  17. data/lib/pwn/ai/agent/dispatch.rb +1 -1
  18. data/lib/pwn/ai/agent/learning.rb +150 -32
  19. data/lib/pwn/ai/agent/loop.rb +291 -129
  20. data/lib/pwn/ai/agent/prompt_builder.rb +47 -29
  21. data/lib/pwn/ai/agent/registry.rb +2 -1
  22. data/lib/pwn/ai/agent/reward.rb +9 -0
  23. data/lib/pwn/ai/agent/swarm.rb +3 -1
  24. data/lib/pwn/ai/agent/task_summarizer.rb +23 -6
  25. data/lib/pwn/ai/agent/tool_guard.rb +109 -0
  26. data/lib/pwn/ai/agent/tools/ruby_eval.rb +44 -20
  27. data/lib/pwn/ai/agent/tools/shell.rb +17 -4
  28. data/lib/pwn/ai/agent/tools/skills.rb +63 -2
  29. data/lib/pwn/ai/anthropic.rb +25 -5
  30. data/lib/pwn/ai/gemini.rb +25 -5
  31. data/lib/pwn/ai/grok.rb +30 -13
  32. data/lib/pwn/ai/http_retry.rb +78 -0
  33. data/lib/pwn/ai/ollama.rb +25 -5
  34. data/lib/pwn/ai/open_ai.rb +26 -13
  35. data/lib/pwn/ai/open_web_ui.rb +25 -5
  36. data/lib/pwn/ai.rb +1 -0
  37. data/lib/pwn/config.rb +1 -1
  38. data/lib/pwn/plugins/log.rb +183 -41
  39. data/lib/pwn/plugins/repl.rb +50 -15
  40. data/lib/pwn/plugins/transparent_browser.rb +16 -2
  41. data/lib/pwn/sessions.rb +50 -1
  42. data/lib/pwn/version.rb +1 -1
  43. data/spec/integration/reinforced_feedback_loop_spec.rb +9 -11
  44. data/spec/lib/pwn/ai/agent/loop_spec.rb +180 -20
  45. data/spec/lib/pwn/ai/agent/prompt_builder_spec.rb +3 -0
  46. data/spec/lib/pwn/ai/agent/registry_spec.rb +1 -1
  47. data/spec/lib/pwn/ai/agent/reward_spec.rb +13 -0
  48. data/spec/lib/pwn/ai/agent/swarm_spec.rb +6 -0
  49. data/spec/lib/pwn/ai/agent/task_summarizer_spec.rb +36 -0
  50. data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +46 -0
  51. data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +24 -0
  52. data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +10 -0
  53. data/spec/lib/pwn/ai/agent/tools/skills_spec.rb +29 -0
  54. data/spec/lib/pwn/ai/grok_spec.rb +22 -0
  55. data/spec/lib/pwn/ai/http_retry_spec.rb +55 -0
  56. data/spec/lib/pwn/plugins/log_spec.rb +170 -19
  57. data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +14 -4
  58. data/spec/lib/pwn/plugins/transparent_browser_spec.rb +8 -0
  59. data/spec/lib/pwn/sessions_spec.rb +24 -0
  60. data/third_party/pwn_rdoc.jsonl +41 -5
  61. data/tmp_pwn_critic_skills_flaw.txt +9 -0
  62. metadata +11 -8
data/lib/pwn/ai/gemini.rb CHANGED
@@ -46,8 +46,8 @@ module PWN
46
46
  http_body = opts[:http_body]
47
47
  http_body ||= {}
48
48
 
49
- timeout = opts[:timeout]
50
- timeout ||= 900
49
+ timeout = PWN::AI::HttpRetry.timeout_s(opts)
50
+ max_attempts = PWN::AI::HttpRetry.max_attempts(opts)
51
51
 
52
52
  spinner = opts[:spinner] || false
53
53
 
@@ -84,11 +84,31 @@ module PWN
84
84
 
85
85
  response.body
86
86
  rescue RestClient::TooManyRequests => e
87
- retry_after = e.response.headers[:retry_after]&.to_i || (0.5 * (retry_count + 1))
88
- sleep(retry_after + rand(0.3..5.0))
89
87
  retry_count += 1
90
-
88
+ if retry_count >= max_attempts
89
+ unless opts[:quiet]
90
+ PWN::AI::HttpRetry.report_event(
91
+ label: 'gemini', which_self: self, quiet: opts[:quiet],
92
+ http_method: http_method, rest_call: rest_call,
93
+ extra: '429 retries exhausted', error: e
94
+ )
95
+ end
96
+ raise "Gemini API Error: #{e.message}: #{e.response}"
97
+ end
98
+ sleep(PWN::AI::HttpRetry.retry_after_s(response: e.response, retry_count: retry_count) + rand(0.3..5.0))
91
99
  retry
100
+ rescue RestClient::Exceptions::Timeout => e
101
+ retry_count += 1
102
+ unless opts[:quiet]
103
+ PWN::AI::HttpRetry.report_event(
104
+ label: 'gemini', which_self: self, quiet: opts[:quiet],
105
+ http_method: http_method, rest_call: rest_call,
106
+ extra: "timeout=#{timeout}s attempt=#{retry_count}/#{max_attempts}", error: e
107
+ )
108
+ end
109
+ retry if retry_count < max_attempts
110
+
111
+ raise "Gemini API Error: #{e.class}: #{e.message}"
92
112
  rescue RestClient::ExceptionWithResponse => e
93
113
  raise "Gemini API Error: #{e.message}: #{e.response}"
94
114
  rescue StandardError => e
data/lib/pwn/ai/grok.rb CHANGED
@@ -341,7 +341,7 @@ module PWN
341
341
  # rest_call: 'required rest call to make per the schema',
342
342
  # params: 'optional params passed in the URI or HTTP Headers',
343
343
  # http_body: 'optional HTTP body sent in HTTP methods that support it e.g. POST',
344
- # timeout: 'optional timeout in seconds (defaults to 900)',
344
+ # timeout: 'optional timeout in seconds (defaults to 180; 5 attempts)',
345
345
  # spinner: 'optional - display spinner (defaults to false)'
346
346
  # )
347
347
  private_class_method def self.grok_rest_call(opts = {})
@@ -415,8 +415,8 @@ module PWN
415
415
  http_body = opts[:http_body]
416
416
  http_body ||= {}
417
417
 
418
- timeout = opts[:timeout]
419
- timeout ||= 900
418
+ timeout = PWN::AI::HttpRetry.timeout_s(opts)
419
+ max_attempts = PWN::AI::HttpRetry.max_attempts(opts)
420
420
 
421
421
  spinner = opts[:spinner] || false
422
422
 
@@ -466,18 +466,35 @@ module PWN
466
466
 
467
467
  response
468
468
  rescue RestClient::TooManyRequests => e
469
- retry_after = e.response.headers[:retry_after]&.to_i ||= (0.5 * (retry_count + 1))
470
- sleep(retry_after + rand(0.3..5.0))
471
469
  retry_count += 1
472
-
470
+ if retry_count >= max_attempts
471
+ unless opts[:quiet]
472
+ PWN::AI::HttpRetry.report_event(
473
+ label: 'grok', which_self: self, quiet: opts[:quiet],
474
+ http_method: http_method, rest_call: rest_call,
475
+ extra: '429 retries exhausted', error: e
476
+ )
477
+ end
478
+ return "#{e.message}: #{e.response}"
479
+ end
480
+ sleep(PWN::AI::HttpRetry.retry_after_s(response: e.response, retry_count: retry_count) + rand(0.3..5.0))
473
481
  retry
482
+ rescue RestClient::Exceptions::Timeout => e
483
+ # Sidecar hops (judge / kind / plan) pass quiet:true and a short
484
+ # timeout. Do not print ERROR: Timed out reading data from server:
485
+ # — ReadTimeout is ExceptionWithResponse with a nil body.
486
+ retry_count += 1
487
+ unless opts[:quiet]
488
+ PWN::AI::HttpRetry.report_event(
489
+ label: 'grok', which_self: self, quiet: opts[:quiet],
490
+ http_method: http_method, rest_call: rest_call,
491
+ extra: "timeout=#{timeout}s attempt=#{retry_count}/#{max_attempts}", error: e
492
+ )
493
+ end
494
+ retry if retry_count < max_attempts
495
+
496
+ nil
474
497
  end
475
- rescue RestClient::Exceptions::Timeout => e
476
- # Sidecar hops (judge / kind / plan) pass quiet:true and a short
477
- # timeout. Do not print ERROR: Timed out reading data from server:
478
- # — ReadTimeout is ExceptionWithResponse with a nil body.
479
- warn "[pwn-ai/grok] #{e.class}: #{e.message} (#{http_method.to_s.upcase} #{rest_call} timeout=#{timeout}s)" unless opts[:quiet]
480
- nil
481
498
  rescue RestClient::ExceptionWithResponse => e
482
499
  puts "ERROR: #{e.message}: #{e.response}" unless opts[:quiet]
483
500
  "#{e.message}: #{e.response}" if opts[:quiet]
@@ -512,7 +529,7 @@ module PWN
512
529
  # tool_choice: 'optional - "auto" | "none" | "required" | {type:"function", function:{name:..}}',
513
530
  # model: 'optional - overrides PWN::Env[:ai][:grok][:model]',
514
531
  # temp: 'optional - temperature (defaults to PWN::Env[:ai][:grok][:temp] || 1)',
515
- # timeout: 'optional - seconds (default 900)',
532
+ # timeout: 'optional - seconds (default 180)',
516
533
  # spinner: 'optional - display spinner (default false)'
517
534
  # )
518
535
  #
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PWN
4
+ module AI
5
+ # Shared REST timeout / 429 / ReadTimeout policy for every AI provider.
6
+ # Default wall clock per attempt is 180s with up to 5 attempts (≈900s total).
7
+ # Short quiet sidecar hops stay single-shot.
8
+ module HttpRetry
9
+ DEFAULT_TIMEOUT_S = 180
10
+ DEFAULT_MAX_ATTEMPTS = 5
11
+
12
+ public_class_method def self.timeout_s(opts = {})
13
+ return DEFAULT_TIMEOUT_S unless opts.is_a?(Hash)
14
+
15
+ t = opts[:timeout].to_i
16
+ t.positive? ? t : DEFAULT_TIMEOUT_S
17
+ end
18
+
19
+ public_class_method def self.max_attempts(opts = {})
20
+ return 1 unless opts.is_a?(Hash)
21
+
22
+ n = opts[:max_attempts].to_i
23
+ return n if n.positive?
24
+ return 1 if opts[:quiet]
25
+ return 1 if opts[:timeout].to_i.positive? && opts[:timeout].to_i < DEFAULT_TIMEOUT_S
26
+
27
+ DEFAULT_MAX_ATTEMPTS
28
+ end
29
+
30
+ public_class_method def self.retry_after_s(opts = {})
31
+ retry_count = [opts[:retry_count].to_i, 1].max
32
+ retry_after = 0
33
+ resp = opts[:response]
34
+ retry_after = resp.headers[:retry_after].to_i if resp.respond_to?(:headers) && resp.headers
35
+ retry_after = (0.5 * retry_count) if retry_after.to_f <= 0
36
+ retry_after
37
+ end
38
+
39
+ # Tees provider REST events into the open pwn-ai DEBUG RN log and STDERR.
40
+ public_class_method def self.report_event(opts = {})
41
+ return unless opts.is_a?(Hash)
42
+
43
+ label = opts[:label].to_s
44
+ label = 'ai' if label.empty?
45
+ err = opts[:error]
46
+ extra = opts[:extra].to_s
47
+ meth = opts[:http_method].to_s.upcase
48
+ call = opts[:rest_call].to_s
49
+ klass = err ? err.class : 'Error'
50
+ msg = err ? err.message : extra
51
+ line = "[pwn-ai/#{label}] #{klass}: #{msg} (#{meth} #{call} #{extra})".strip
52
+ which = opts[:which_self] || self
53
+ PWN::Plugins::Log.progress(msg: line, which_self: which, cap: 0) if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.debug_enabled?
54
+ warn line unless opts[:quiet]
55
+ line
56
+ end
57
+
58
+ public_class_method def self.authors
59
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
60
+ end
61
+
62
+ public_class_method def self.help
63
+ puts <<~USAGE
64
+ USAGE:
65
+ PWN::AI::HttpRetry.timeout_s(timeout: nil) # => 180
66
+ PWN::AI::HttpRetry.max_attempts(quiet: false) # => 5
67
+ PWN::AI::HttpRetry.report_event(
68
+ label: 'grok',
69
+ error: e,
70
+ http_method: :post,
71
+ rest_call: 'chat/completions'
72
+ )
73
+ #{self}.authors
74
+ USAGE
75
+ end
76
+ end
77
+ end
78
+ end
data/lib/pwn/ai/ollama.rb CHANGED
@@ -65,8 +65,8 @@ module PWN
65
65
  http_body = opts[:http_body]
66
66
  http_body ||= {}
67
67
 
68
- timeout = opts[:timeout]
69
- timeout ||= 900
68
+ timeout = PWN::AI::HttpRetry.timeout_s(opts)
69
+ max_attempts = PWN::AI::HttpRetry.max_attempts(opts)
70
70
 
71
71
  spinner = opts[:spinner] || false
72
72
  stream = http_body.is_a?(Hash) && http_body[:stream] == true
@@ -173,11 +173,31 @@ module PWN
173
173
  end
174
174
  response
175
175
  rescue RestClient::TooManyRequests => e
176
- retry_after = e.response.headers[:retry_after]&.to_i ||= (0.5 * (retry_count + 1))
177
- sleep(retry_after + rand(0.3..5.0))
178
176
  retry_count += 1
179
-
177
+ if retry_count >= max_attempts
178
+ unless opts[:quiet]
179
+ PWN::AI::HttpRetry.report_event(
180
+ label: 'ollama', which_self: self, quiet: opts[:quiet],
181
+ http_method: http_method, rest_call: rest_call,
182
+ extra: '429 retries exhausted', error: e
183
+ )
184
+ end
185
+ return "#{e.message}: #{e.response}"
186
+ end
187
+ sleep(PWN::AI::HttpRetry.retry_after_s(response: e.response, retry_count: retry_count) + rand(0.3..5.0))
180
188
  retry
189
+ rescue RestClient::Exceptions::Timeout => e
190
+ retry_count += 1
191
+ unless opts[:quiet]
192
+ PWN::AI::HttpRetry.report_event(
193
+ label: 'ollama', which_self: self, quiet: opts[:quiet],
194
+ http_method: http_method, rest_call: rest_call,
195
+ extra: "timeout=#{timeout}s attempt=#{retry_count}/#{max_attempts}", error: e
196
+ )
197
+ end
198
+ retry if retry_count < max_attempts
199
+
200
+ nil
181
201
  end
182
202
  rescue RestClient::ExceptionWithResponse => e
183
203
  body = begin
@@ -451,8 +451,8 @@ module PWN
451
451
  http_body = opts[:http_body]
452
452
  http_body ||= {}
453
453
 
454
- timeout = opts[:timeout]
455
- timeout ||= 900
454
+ timeout = PWN::AI::HttpRetry.timeout_s(opts)
455
+ max_attempts = PWN::AI::HttpRetry.max_attempts(opts)
456
456
 
457
457
  spinner = opts[:spinner] || false
458
458
 
@@ -502,21 +502,34 @@ module PWN
502
502
  end
503
503
  response
504
504
  rescue RestClient::TooManyRequests => e
505
- duration = 0
506
- if e.response
507
- retry_after = e.response.headers[:retry_after]&.to_i ||= (0.5 * (retry_count + 1))
508
- duration = retry_after.to_i
505
+ retry_count += 1
506
+ if retry_count >= max_attempts
507
+ unless opts[:quiet]
508
+ PWN::AI::HttpRetry.report_event(
509
+ label: 'openai', which_self: self, quiet: opts[:quiet],
510
+ http_method: http_method, rest_call: rest_call,
511
+ extra: '429 retries exhausted', error: e
512
+ )
513
+ end
514
+ return "#{e.message}: #{e.response}"
509
515
  end
510
- sleep(duration + rand(0.3..5.0))
516
+ sleep(PWN::AI::HttpRetry.retry_after_s(response: e.response, retry_count: retry_count) + rand(0.3..5.0))
517
+ retry
518
+ rescue RestClient::Exceptions::Timeout => e
519
+ # Sidecar hops pass quiet:true. Never print
520
+ # ERROR: Timed out reading data from server:
511
521
  retry_count += 1
522
+ unless opts[:quiet]
523
+ PWN::AI::HttpRetry.report_event(
524
+ label: 'openai', which_self: self, quiet: opts[:quiet],
525
+ http_method: http_method, rest_call: rest_call,
526
+ extra: "timeout=#{timeout}s attempt=#{retry_count}/#{max_attempts}", error: e
527
+ )
528
+ end
529
+ retry if retry_count < max_attempts
512
530
 
513
- retry
531
+ nil
514
532
  end
515
- rescue RestClient::Exceptions::Timeout => e
516
- # Sidecar hops pass quiet:true. Never print
517
- # ERROR: Timed out reading data from server:
518
- warn "[pwn-ai/openai] #{e.class}: #{e.message} (#{http_method.to_s.upcase} #{rest_call} timeout=#{timeout}s)" unless opts[:quiet]
519
- nil
520
533
  rescue RestClient::ExceptionWithResponse => e
521
534
  puts "ERROR: #{e.message}: #{e.response}" unless opts[:quiet]
522
535
  "#{e.message}: #{e.response}" if opts[:quiet]
@@ -77,8 +77,8 @@ module PWN
77
77
  http_body = opts[:http_body]
78
78
  http_body ||= {}
79
79
 
80
- timeout = opts[:timeout]
81
- timeout ||= 900
80
+ timeout = PWN::AI::HttpRetry.timeout_s(opts)
81
+ max_attempts = PWN::AI::HttpRetry.max_attempts(opts)
82
82
 
83
83
  spinner = opts[:spinner] || false
84
84
  stream = http_body.is_a?(Hash) && http_body[:stream] == true
@@ -198,11 +198,31 @@ module PWN
198
198
 
199
199
  response
200
200
  rescue RestClient::TooManyRequests => e
201
- retry_after = e.response.headers[:retry_after]&.to_i ||= (0.5 * (retry_count + 1))
202
- sleep(retry_after + rand(0.3..5.0))
203
201
  retry_count += 1
204
-
202
+ if retry_count >= max_attempts
203
+ unless opts[:quiet]
204
+ PWN::AI::HttpRetry.report_event(
205
+ label: 'openwebui', which_self: self, quiet: opts[:quiet],
206
+ http_method: http_method, rest_call: rest_call,
207
+ extra: '429 retries exhausted', error: e
208
+ )
209
+ end
210
+ return "#{e.message}: #{e.response}"
211
+ end
212
+ sleep(PWN::AI::HttpRetry.retry_after_s(response: e.response, retry_count: retry_count) + rand(0.3..5.0))
205
213
  retry
214
+ rescue RestClient::Exceptions::Timeout => e
215
+ retry_count += 1
216
+ unless opts[:quiet]
217
+ PWN::AI::HttpRetry.report_event(
218
+ label: 'openwebui', which_self: self, quiet: opts[:quiet],
219
+ http_method: http_method, rest_call: rest_call,
220
+ extra: "timeout=#{timeout}s attempt=#{retry_count}/#{max_attempts}", error: e
221
+ )
222
+ end
223
+ retry if retry_count < max_attempts
224
+
225
+ nil
206
226
  end
207
227
  rescue RestClient::ExceptionWithResponse => e
208
228
  # Never return nil here — chat_with_tools used to `return nil if response.nil?`
data/lib/pwn/ai.rb CHANGED
@@ -9,6 +9,7 @@ module PWN
9
9
  autoload :Anthropic, 'pwn/ai/anthropic'
10
10
  autoload :Gemini, 'pwn/ai/gemini'
11
11
  autoload :Grok, 'pwn/ai/grok'
12
+ autoload :HttpRetry, 'pwn/ai/http_retry'
12
13
  autoload :Ollama, 'pwn/ai/ollama'
13
14
  autoload :OpenAI, 'pwn/ai/open_ai'
14
15
  autoload :OpenWebUI, 'pwn/ai/open_web_ui'
data/lib/pwn/config.rb CHANGED
@@ -160,7 +160,7 @@ module PWN
160
160
  # LLM tangible-task decomposition for autonomous goals (default on).
161
161
  task_summary_llm: nil,
162
162
  tool_router: nil, # nil = auto (true when :active is local :ollama/:openwebui) — cuts ~11k→~3k schema tokens
163
- tool_preference: %w[memory_recall session_recall skills_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember],
163
+ tool_preference: %w[memory_recall session_recall skills_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember skills_update],
164
164
  escalation_persona: 'escalator', # Swarm persona for frontier corrective hints when a local model is stuck
165
165
  # sample E3 verify_as_reward: true|false|nil(auto: ~10% local / always frontier when CLAIM_RX hits)
166
166
  verify_as_reward: nil,