pwn 0.5.697 → 0.5.699

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: 4218feff0995079766c3b31b6beec85c9b45b1f971ac0bc78a9794c67ea8016e
4
- data.tar.gz: 5a47207b366764a60b8c560bdcf77c5b12288d7fdbeb3e2d74c655a8e92e12e3
3
+ metadata.gz: 5d7edeea1940ad56143190285565ba9bd76f4d78a47294238eafaad1d002686f
4
+ data.tar.gz: c3a594906ca8d239b8f5dfc3fe3343ec718bf1a71e3c85334b3b6db7e852b5fe
5
5
  SHA512:
6
- metadata.gz: 326c061669305b7069c4a66e68d958b6935ef3a9c3f90e02ee1242ad6bedfed9e5e31170fd4ae8b7b9570ab7c63fb1e7198c0cc97a5a8e3eeb96400bc329c611
7
- data.tar.gz: ed8cdb221e58fdb00ae799d902121e0f2b6f629871ceb458a54ef35ee5e2ad326070c431d9c17346db442e0d62374f90f85cba074a00967d3193d41753a091a7
6
+ metadata.gz: fee1edba85d565609b592498feff5eaf38bc01a4771af2a2e22f3719d1eac7faaf54c8f8772837358cf5cbf18db59736aee90fe4a7114c1e6353be423d42e2e8
7
+ data.tar.gz: 4ad32cb15b7bd78d0fa150cf63379ecf61184b84a902426a3581a558b48b17b0a7bc23048a2d2576155b538403e9472a1ebe542a8154eece95a36e4106e352f9
@@ -35,6 +35,7 @@ PWN::AI::HttpRetry.timeout_s(opts)
35
35
 
36
36
  - `timeout_s`
37
37
  - `max_attempts`
38
+ - `retryable`
38
39
  - `retry_after_s`
39
40
  - `report_event`
40
41
  - `authors`
@@ -851,6 +851,17 @@ module PWN
851
851
  opts[:session_id].to_s
852
852
  end
853
853
 
854
+ private_class_method def self.engine_transient?(opts = {})
855
+ err = opts[:error]
856
+ return false unless err
857
+
858
+ return PWN::AI::HttpRetry.retryable?(error: err) if defined?(PWN::AI::HttpRetry) && PWN::AI::HttpRetry.respond_to?(:retryable?)
859
+
860
+ err.message.to_s.match?(/HTTP 50[234]|Gateway Time-out|stream absolute timeout/i)
861
+ rescue StandardError
862
+ false
863
+ end
864
+
854
865
  # E1 — did the environment change under this tool? If Metrics CUSUM
855
866
  # tripped for it in the last hour AND Extrospection.drift shows a
856
867
  # toolchain/net/repo change, blame the WORLD not the AGENT.
@@ -1416,8 +1427,10 @@ module PWN
1416
1427
  messages = opts[:messages]
1417
1428
  return messages unless messages.is_a?(Array) && messages.length > 12
1418
1429
 
1419
- keep_pairs = (agent_flag(key: :history_keep_tool_pairs, default: 6) || 6).to_i
1420
- max_chars = (agent_flag(key: :history_tool_max_chars, default: 2_000) || 2_000).to_i
1430
+ keep_pairs = opts[:keep_pairs]
1431
+ keep_pairs = (agent_flag(key: :history_keep_tool_pairs, default: 6) || 6).to_i if keep_pairs.nil?
1432
+ max_chars = opts[:max_chars]
1433
+ max_chars = (agent_flag(key: :history_tool_max_chars, default: 2_000) || 2_000).to_i if max_chars.nil?
1421
1434
 
1422
1435
  head = []
1423
1436
  rest = messages.dup
@@ -2473,20 +2486,43 @@ module PWN
2473
2486
 
2474
2487
  turn_fails = Hash.new(0)
2475
2488
  escalated = false
2489
+ engine_blips = 0
2476
2490
  maybe_park_budget_scars!
2477
2491
  maybe_extinguish_parked!
2478
2492
 
2479
2493
  i = 0
2480
2494
  loop do
2481
2495
  i += 1
2482
- # 3.1 — compact history on local so tool dumps don't fill num_ctx
2483
- compact_history!(messages: messages) if local
2496
+ # 3.1 — compact fat tool dumps so remote ReadTimeout hops do not
2497
+ # retry the same 70-message payload (R1 201215 Anthropic 180s×5).
2498
+ compact_history!(messages: messages)
2484
2499
  # English-task-as-primary: when plan_idx advanced, tell the model
2485
2500
  # which plain-English task is active before the next tool batch.
2486
2501
  inject_task_focus!(messages: messages, state: ts_state, request: request) unless skip_compass
2487
2502
 
2488
2503
  t0 = Time.now
2489
- msg = call_engine(messages: messages, tools: tools, ts_state: ts_state)
2504
+ begin
2505
+ msg = call_engine(messages: messages, tools: tools, ts_state: ts_state)
2506
+ rescue StandardError => e
2507
+ if engine_transient?(error: e)
2508
+ engine_blips += 1
2509
+ debug_progress(msg: "engine hop failed #{e.class}: #{e.message.to_s[0, 240]} blip=#{engine_blips}")
2510
+ if engine_blips >= 5
2511
+ txt = "[pwn-ai] engine hop failed after #{engine_blips} tries: #{e.message.to_s[0, 240]}"
2512
+ debug_final_text!(text: txt)
2513
+ final_chars = txt.length
2514
+ return txt
2515
+ end
2516
+ compact_history!(messages: messages, keep_pairs: 3, max_chars: 800)
2517
+ messages << {
2518
+ role: 'user',
2519
+ content: '[pwn-ai] engine hop failed (transient). Keep calling CORE_TOOLS. ' \
2520
+ "Do not stop. (#{e.message.to_s[0, 180]})"
2521
+ }
2522
+ next
2523
+ end
2524
+ raise
2525
+ end
2490
2526
  engine_s += (Time.now - t0)
2491
2527
  PWN::Plugins::TTYSpinner.halt_all! if defined?(PWN::Plugins::TTYSpinner)
2492
2528
  if msg.nil?
@@ -573,8 +573,8 @@ module PWN
573
573
  # model cannot stall the user's real answer, and stay quiet on timeout.
574
574
  private_class_method def self.sidecar_timeout
575
575
  n = (PWN::Env.dig(:ai, :agent, :sidecar_llm_timeout) if defined?(PWN::Env)).to_i
576
- n = 20 if n < 5
577
- n = 60 if n > 60
576
+ n = 180 if n < 30
577
+ n = 180 if n > 180
578
578
  n
579
579
  rescue StandardError
580
580
  20
@@ -661,6 +661,17 @@ module PWN
661
661
  ]
662
662
  end
663
663
 
664
+ artifact = goal_text[%r{(?:/(?:tmp|var|home|opt|usr)/\S+\.(?:pdf|html|md|json|txt|csv)|~/\S+\.(?:pdf|html|md|json|txt|csv))}i]
665
+ if artifact && goal_lc.match?(/\b(analy[sz]e|test|scan|report|generat|store|write|export)\b/)
666
+ tasks << 'Carry out the requested analysis using the named skills and live evidence'
667
+ tasks << "Write the requested report to #{artifact}"
668
+ if goal_lc.match?(/\b(json|ya?ml|table|csv|tsv)\b/)
669
+ fmt = goal_lc[/\b(json|ya?ml|table|csv|tsv)\b/]
670
+ tasks << "Present the results in #{fmt} format"
671
+ end
672
+ return tasks
673
+ end
674
+
664
675
  tasks << "Understand the request: #{truncate_goal(goal: goal_text)}"
665
676
  tasks << "Carry out the core work for: #{truncate_goal(goal: goal_text)}"
666
677
 
@@ -24,6 +24,7 @@ module PWN
24
24
  # Conservative bash-only constructs. POSIX `$(())` is allowed.
25
25
  BASHISM_RX = /
26
26
  \bPIPESTATUS\b
27
+ |\$\{?RANDOM\}?\b
27
28
  |\[\[(?:\s|\z)
28
29
  |(?:^|[\s;|&])source\s+\S
29
30
  |<\([^)]
@@ -65,7 +65,7 @@ PWN::AI::Agent::Registry.register(
65
65
 
66
66
  if PWN::AI::Agent::ToolGuard.bashism?(text: cmd) && !PWN::AI::Agent::ToolGuard.shell_bash?
67
67
  return PWN::AI::Agent::ToolGuard.invalid_payload(
68
- hint: 'Command uses bash-only syntax (PIPESTATUS, [[ ]], process substitution, ' \
68
+ hint: 'Command uses bash-only syntax (PIPESTATUS, $RANDOM, [[ ]], process substitution, ' \
69
69
  'source, &>). This handler runs /bin/sh (dash) unless ' \
70
70
  'PWN::Env[:ai][:agent][:shell_bash]=true. Rewrite as POSIX or opt in to bash.'
71
71
  )
@@ -27,6 +27,15 @@ module PWN
27
27
  DEFAULT_MAX_ATTEMPTS
28
28
  end
29
29
 
30
+ public_class_method def self.retryable?(opts = {})
31
+ err = opts[:error]
32
+ msg = err.respond_to?(:message) ? err.message.to_s : err.to_s
33
+ msg = opts[:message].to_s if msg.empty?
34
+ msg.match?(/HTTP 50[234]|Gateway Time-out|stream absolute timeout|ReadTimeout|Net::ReadTimeout|Timed out reading/i)
35
+ rescue StandardError
36
+ false
37
+ end
38
+
30
39
  public_class_method def self.retry_after_s(opts = {})
31
40
  retry_count = [opts[:retry_count].to_i, 1].max
32
41
  retry_after = 0
data/lib/pwn/ai/ollama.rb CHANGED
@@ -198,6 +198,21 @@ module PWN
198
198
  retry if retry_count < max_attempts
199
199
 
200
200
  nil
201
+ rescue StandardError => e
202
+ if PWN::AI::HttpRetry.retryable?(error: e) && retry_count + 1 < max_attempts
203
+ retry_count += 1
204
+ unless opts[:quiet]
205
+ PWN::AI::HttpRetry.report_event(
206
+ label: 'ollama', which_self: self, quiet: opts[:quiet],
207
+ http_method: http_method, rest_call: rest_call,
208
+ extra: "retryable attempt=#{retry_count}/#{max_attempts}",
209
+ error: e
210
+ )
211
+ end
212
+ sleep(PWN::AI::HttpRetry.retry_after_s(retry_count: retry_count) + rand(0.3..2.0))
213
+ retry
214
+ end
215
+ raise e
201
216
  end
202
217
  rescue RestClient::ExceptionWithResponse => e
203
218
  body = begin
@@ -1284,8 +1284,9 @@ module PWN
1284
1284
  raise
1285
1285
  rescue StandardError => e
1286
1286
  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!)
1287
- warn "[pwn-ai] native agent loop failed (#{e.class}: #{e.message}\n#{e.backtrace}); " \
1288
- 'falling back to legacy regex-ReAct.'
1287
+ warn "[pwn-ai] native agent loop failed (#{e.class}: #{e.message.to_s.split("\n").first})"
1288
+ request.replace('nil')
1289
+ next
1289
1290
  ensure
1290
1291
  PWN::Plugins::REPL.ready_tty!
1291
1292
  end
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.697'
4
+ VERSION = '0.5.699'
5
5
  end
@@ -424,9 +424,43 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
424
424
  expect(src).to match(/note_exception!/)
425
425
  expect(src).to match(/rescue StandardError/)
426
426
  expect(src).to include("debug_progress(msg: 'engine returned no message')")
427
+ expect(src).to match(/engine_transient|retryable\?/)
427
428
  expect(src).not_to match(/dispatch #\{name\} ok_len=/)
428
429
  end
429
430
 
431
+ it 'keeps Loop.run going after a transient engine 504' do
432
+ src = File.read(described_class.method(:run).source_location.first)
433
+ expect(src).to match(/engine_transient\?|HttpRetry\.retryable\?/)
434
+ expect(src).to match(/engine hop failed|engine_blip/)
435
+ end
436
+
437
+ it 'compacts tool history for every engine, not only local' do
438
+ src = File.read(described_class.method(:run).source_location.first)
439
+ expect(src).to match(/compact_history!\(messages: messages\)/)
440
+ expect(src).not_to match(/compact_history!\(messages: messages\) if local/)
441
+ end
442
+
443
+ it 'shrinks tool bodies when history is fat' do
444
+ msgs = [
445
+ { role: 'system', content: 'sys' },
446
+ { role: 'user', content: 'goal' }
447
+ ]
448
+ 8.times do |n|
449
+ msgs << { role: 'assistant', content: '', tool_calls: [{ id: "c#{n}" }] }
450
+ msgs << { role: 'tool', tool_call_id: "c#{n}", name: 'shell', content: ('x' * 4_000) }
451
+ end
452
+ out = described_class.send(:compact_history!, messages: msgs)
453
+ tool_bodies = out.select { |m| m[:role].to_s == 'tool' }
454
+ expect(tool_bodies.length).to be <= 6
455
+ expect(tool_bodies.map { |m| m[:content].to_s.length }.max).to be <= 2_100
456
+ end
457
+
458
+ it 'compacts again after a transient engine hop before retrying' do
459
+ src = File.read(described_class.method(:run).source_location.first)
460
+ hop = src[/if engine_transient\?.*?raise/m]
461
+ expect(hop).to match(/compact_history!/)
462
+ end
463
+
430
464
  it 'records a timeout increment mistake instead of treating success:true as ok' do
431
465
  tmp = Dir.mktmpdir
432
466
  stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
@@ -36,6 +36,14 @@ describe 'P0 signal hygiene (handler + inbox + policy-cold + calibrate)' do
36
36
  expect(r[:hint].to_s).to match(/bash-only/i)
37
37
  end
38
38
 
39
+ it 'rejects $RANDOM under /bin/sh so the model rewrites instead of getting USER=' do
40
+ expect(PWN::AI::Agent::ToolGuard.bashism?(text: 'U="p4tester$RANDOM"; echo $U')).to eq(true)
41
+ handler = PWN::AI::Agent::Registry.lookup(name: 'shell').handler
42
+ r = handler.call(command: 'U="p4tester$RANDOM"; echo $U')
43
+ expect(r[:error]).to eq('invalid_payload')
44
+ expect(r[:hint].to_s).to match(/RANDOM|bash-only|POSIX/i)
45
+ end
46
+
39
47
  it 'does not refuse pwn_eval host-discovery as unauthorized' do
40
48
  src = File.read('/opt/pwn/lib/pwn/ai/agent/tools/ruby_eval.rb')
41
49
  expect(src).not_to match(/recon_blocked/)
@@ -425,6 +425,16 @@ describe PWN::AI::Agent::TaskSummarizer do
425
425
  )
426
426
  end
427
427
 
428
+ it 'splits analyze-plus-artifact goals instead of pasting the full request twice' do
429
+ goal = 'Exhaustively analyze the running application for vulnerabilities. ' \
430
+ 'Once complete, generate a complete PDF report and store in /tmp/container_pentest-p4.pdf'
431
+ tasks = described_class.fallback_decompose(goal: goal)
432
+ expect(tasks.length).to be >= 2
433
+ expect(tasks.join("\n")).to include('/tmp/container_pentest-p4.pdf')
434
+ expect(tasks.grep(/Understand the request:/).length).to eq(0)
435
+ expect(tasks.grep(/Carry out the core work for:/).length).to eq(0)
436
+ end
437
+
428
438
  it 'accepts injected :tasks / :llm_tasks without network I/O' do
429
439
  custom = ['step alpha', 'step beta', 'verify completion']
430
440
  tasks = described_class.plan(request: 'anything at all', tasks: custom)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'stringio'
5
+ require 'rest-client'
5
6
 
6
7
  describe PWN::AI::HttpRetry do
7
8
  it 'defaults timeout to 180 and max_attempts to 5' do
@@ -14,6 +15,12 @@ describe PWN::AI::HttpRetry do
14
15
  expect(described_class.max_attempts(max_attempts: 3)).to eq(3)
15
16
  end
16
17
 
18
+ it 'treats gateway 504 and stream absolute timeouts as retryable' do
19
+ expect(described_class.retryable?(error: 'ERROR: Ollama HTTP 504 for api/chat: Gateway Time-out')).to eq(true)
20
+ expect(described_class.retryable?(error: 'ERROR: Ollama stream absolute timeout after 20s for api/chat')).to eq(true)
21
+ expect(described_class.retryable?(error: '400 Bad Request')).to eq(false)
22
+ end
23
+
17
24
  it 'tees emit lines into the open debug request log' do
18
25
  path = PWN::Plugins::Log.start_debug(
19
26
  tee: StringIO.new,
@@ -50,6 +57,7 @@ describe PWN::AI::HttpRetry do
50
57
  expect(src).not_to match(/timeout \|\|= 900/)
51
58
  expect(src).to match(/Exceptions::Timeout/)
52
59
  expect(src).to match(/HttpRetry\.report_event/)
60
+ expect(src).to match(/retryable\?|HTTP 50/) if name == 'PWN::AI::Ollama'
53
61
  end
54
62
  end
55
63
  end
@@ -174,4 +174,14 @@ describe 'pwn-ai debug final answer placement' do
174
174
  expect(hook).to include('→ pwn-ai →')
175
175
  expect(hook).to include('→ result')
176
176
  end
177
+
178
+ it 'does not fall through to regex-ReAct after a native Loop.run error' do
179
+ native_start = hook.index('final = PWN::AI::Agent::Loop.run')
180
+ rescue_at = hook.index('rescue StandardError', native_start)
181
+ expect(rescue_at).not_to be_nil
182
+ chunk = hook[rescue_at, 900]
183
+ expect(chunk).to match(/request\.replace\('nil'\)/)
184
+ expect(chunk).to match(/\bnext\b/)
185
+ expect(chunk).not_to include('legacy regex-ReAct')
186
+ end
177
187
  end
@@ -339,6 +339,7 @@
339
339
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.degrade_text_only Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.degrade_text_only`: "}]}
340
340
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.dispatch_fail_n Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.dispatch_fail_n`: "}]}
341
341
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.emit_task_summary Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.emit_task_summary`: "}]}
342
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.engine_transient? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.engine_transient?`: "}]}
342
343
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.escalate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.escalate`: "}]}
343
344
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_enough_to_finalize? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_enough_to_finalize?`: "}]}
344
345
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.expose_current_session Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.expose_current_session`: "}]}
@@ -793,6 +794,7 @@
793
794
  {"messages":[{"role":"user","content":"PWN::AI::HttpRetry.max_attempts Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.max_attempts`: "}]}
794
795
  {"messages":[{"role":"user","content":"PWN::AI::HttpRetry.report_event Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.report_event`: "}]}
795
796
  {"messages":[{"role":"user","content":"PWN::AI::HttpRetry.retry_after_s Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.retry_after_s`: "}]}
797
+ {"messages":[{"role":"user","content":"PWN::AI::HttpRetry.retryable? Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.retryable?`: "}]}
796
798
  {"messages":[{"role":"user","content":"PWN::AI::HttpRetry.timeout_s Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.timeout_s`: "}]}
797
799
  {"messages":[{"role":"user","content":"PWN::AI::Ollama.assemble_native_chat_stream Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.assemble_native_chat_stream`: "}]}
798
800
  {"messages":[{"role":"user","content":"PWN::AI::Ollama.assemble_ollama_stream Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.assemble_ollama_stream`: "}]}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pwn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.697
4
+ version: 0.5.699
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.