pwn 0.5.704 → 0.5.706

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: 4bd3437e2c7ee97e783659b63d69dd21368ce47a284c29f97b6d8ddd64631134
4
- data.tar.gz: 9e71c1bbabee3abd02f3d4f252fa1eed48854f2c2379b2df914515ed2a593775
3
+ metadata.gz: f9233042c1c9518d8cfdcbfe6499eb521cb0348384554e03ab6641abdda253b9
4
+ data.tar.gz: 51b81fd36f292bc54e762d4bb47f5641c9b263addf9f37997aaa4c2f6a00c49f
5
5
  SHA512:
6
- metadata.gz: ffb5ae45ac796c161ab9fa29816c823e1f937404f845a23bf3757d412f82a5ccbe950d93bfe6b1771af1ee8ffefd9b8374fbda07024bec7278195c969a04f0be
7
- data.tar.gz: 649a698b9da06319331c4d3289c603c0907e763f6a4a2a508f6c1a97426a969483fea38766002dbf364830a574cc0b5012d44ae8ea1881303c61acae4eb427b7
6
+ metadata.gz: 370d8b9305f7dcd2a692061b6708feca282546109533131d244cd97d207d1b216afac16baf44847a8a098c9847467208dd30e81084729f79ee7e3c16c050ae32
7
+ data.tar.gz: 97725299f78baef12d73614b7258ebf5cc3664316c82d2bf53d6a82e3d9e0e0d2542a3143cbc686fc75b46b6883fb8edc0174cfd38ce74b88a0c908ab4864642
data/Gemfile CHANGED
@@ -89,8 +89,8 @@ 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.151.0'
93
- gem 'selenium-webdriver', '4.47.0'
92
+ gem 'selenium-devtools', '0.152.0'
93
+ gem 'selenium-webdriver', '4.48.0'
94
94
  gem 'slack-ruby-client', '3.2.0'
95
95
  gem 'socksify', '1.8.1'
96
96
  gem 'spreadsheet', '1.3.5'
@@ -584,6 +584,10 @@ module PWN
584
584
  live = effects.reject { |fx| %i[recall store].include?(fx) }
585
585
  return true if live.empty?
586
586
  return true if duration_unsatisfied?(request: request)
587
+ return true if declared_contract_unsatisfied?(
588
+ request: request,
589
+ messages: opts[:messages]
590
+ )
587
591
  return true if need == :write && !write_verified?(effects: effects)
588
592
  return true if need == :browse && !effects.include?(:browse)
589
593
  return true if need == :any && !effects.intersect?(%i[write browse eval])
@@ -613,23 +617,157 @@ module PWN
613
617
  }.freeze
614
618
 
615
619
  private_class_method def self.duration_unsatisfied?(opts = {})
616
- req = opts[:request].to_s
617
- hours = nil
618
- if (m = req.match(/\b(\d+)\s*(?:hours?|hrs?)\b/i))
619
- hours = m[1].to_i
620
- elsif (m = req.match(/\b(twenty-four|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty)\s*(?:hours?|hrs?)\b/i))
621
- hours = HOUR_WORDS[m[1].downcase]
620
+ secs = declared_min_seconds(request: opts[:request])
621
+ if secs <= 0
622
+ req = opts[:request].to_s
623
+ hours = nil
624
+ if (m = req.match(/\b(\d+)\s*(?:hours?|hrs?)\b/i))
625
+ hours = m[1].to_i
626
+ elsif (m = req.match(/\b(twenty-four|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty)\s*(?:hours?|hrs?)\b/i))
627
+ hours = HOUR_WORDS[m[1].downcase]
628
+ end
629
+ secs = hours.to_i * 3600
622
630
  end
623
- return false if hours.to_i <= 0
631
+ return false if secs <= 0
624
632
 
625
633
  t0 = Thread.current[:pwn_loop_t0]
626
634
  return false unless t0
627
635
 
628
- (Time.now - t0) < (hours * 3600)
636
+ (Time.now - t0) < secs
637
+ rescue StandardError
638
+ false
639
+ end
640
+
641
+ EMPTY_CONTRACT = {
642
+ paths: [],
643
+ min_seconds: 0,
644
+ skills: [],
645
+ proofs: [],
646
+ hosts: []
647
+ }.freeze
648
+
649
+ private_class_method def self.declared_min_seconds(opts = {})
650
+ declared_contract(request: opts[:request])[:min_seconds].to_i
651
+ end
652
+
653
+ private_class_method def self.declared_contract_unsatisfied?(opts = {})
654
+ contract = declared_contract(request: opts[:request])
655
+ files = Array(contract[:paths]) + Array(contract[:proofs])
656
+ return true if files.any? { |path| deliverable_missing?(path: path) }
657
+ return true if declared_skills_missing?(skills: contract[:skills])
658
+ return true if declared_hosts_missing?(hosts: contract[:hosts], messages: opts[:messages])
659
+
660
+ false
629
661
  rescue StandardError
630
662
  false
631
663
  end
632
664
 
665
+ private_class_method def self.declared_skills_missing?(opts = {})
666
+ names = Array(opts[:skills]).map(&:to_s).reject(&:empty?)
667
+ return false if names.empty?
668
+ return true unless defined?(PWN::Skills) && PWN::Skills.is_a?(Hash)
669
+
670
+ have = PWN::Skills.keys.map(&:to_s)
671
+ names.any? { |name| !have.include?(name) }
672
+ end
673
+
674
+ private_class_method def self.declared_hosts_missing?(opts = {})
675
+ hosts = Array(opts[:hosts]).map(&:to_s).reject(&:empty?)
676
+ return false if hosts.empty?
677
+
678
+ blob = Array(opts[:messages]).select { |msg| msg.is_a?(Hash) && msg[:role].to_s == 'tool' }
679
+ .map { |msg| msg[:content].to_s }
680
+ .join("\n")
681
+ .downcase
682
+ hosts.any? { |host| !blob.include?(host.to_s.downcase) }
683
+ end
684
+
685
+ private_class_method def self.declared_deliverables(opts = {})
686
+ Array(declared_contract(request: opts[:request])[:paths])
687
+ end
688
+
689
+ private_class_method def self.declared_contract(opts = {})
690
+ cached = Thread.current[:pwn_loop_deliverables]
691
+ return normalize_contract(raw: cached) if cached.is_a?(Array) || cached.is_a?(Hash)
692
+ return EMPTY_CONTRACT.dup unless Thread.current[:pwn_loop_active]
693
+
694
+ contract = infer_deliverables(request: opts[:request])
695
+ Thread.current[:pwn_loop_deliverables] = contract
696
+ contract
697
+ end
698
+
699
+ private_class_method def self.normalize_contract(opts = {})
700
+ raw = opts[:raw]
701
+ return EMPTY_CONTRACT.merge(paths: raw.map(&:to_s).select { |p| p.start_with?('/') }) if raw.is_a?(Array)
702
+ return EMPTY_CONTRACT.dup unless raw.is_a?(Hash)
703
+
704
+ hours = raw[:hours] || raw['hours']
705
+ secs = (raw[:min_seconds] || raw['min_seconds']).to_i
706
+ secs = hours.to_i * 3600 if secs <= 0 && hours.to_i.positive?
707
+ {
708
+ paths: abs_paths(rows: raw[:paths] || raw['paths']),
709
+ min_seconds: secs,
710
+ skills: Array(raw[:skills] || raw['skills']).map(&:to_s).reject(&:empty?).uniq,
711
+ proofs: abs_paths(rows: raw[:proofs] || raw['proofs']),
712
+ hosts: Array(raw[:hosts] || raw['hosts']).map(&:to_s).reject(&:empty?).uniq
713
+ }
714
+ end
715
+
716
+ private_class_method def self.abs_paths(opts = {})
717
+ Array(opts[:rows]).map(&:to_s).select { |path| path.start_with?('/') }.uniq
718
+ end
719
+
720
+ private_class_method def self.infer_deliverables(opts = {})
721
+ request = opts[:request].to_s
722
+ return EMPTY_CONTRACT.dup if request.strip.empty?
723
+
724
+ reply = call_engine(
725
+ messages: [
726
+ {
727
+ role: 'system',
728
+ content: 'Reply with JSON only. No markdown. No tools.'
729
+ },
730
+ {
731
+ role: 'user',
732
+ content: "Operator request:\n#{request}\n\n" \
733
+ 'When that request is complete, what must be true on this host? ' \
734
+ 'JSON only: {"paths":["/abs/file"],"min_seconds":0,"skills":["name"],' \
735
+ '"proofs":["/abs/poc"],"hosts":["ip-or-hostname"]}. ' \
736
+ 'Use [] or 0 when a field is not required. Do not invent work. Paths must be absolute.'
737
+ }
738
+ ],
739
+ tools: nil
740
+ )
741
+ text = reply.is_a?(Hash) ? (reply[:content] || reply['content']).to_s : reply.to_s
742
+ parse_contract(text: text)
743
+ rescue StandardError
744
+ EMPTY_CONTRACT.dup
745
+ end
746
+
747
+ private_class_method def self.parse_contract(opts = {})
748
+ text = opts[:text].to_s
749
+ json = nil
750
+ begin
751
+ json = JSON.parse(text, symbolize_names: true)
752
+ rescue JSON::ParserError
753
+ start = text.index('{')
754
+ stop = text.rindex('}')
755
+ return EMPTY_CONTRACT.dup unless start && stop && stop > start
756
+
757
+ json = JSON.parse(text[start..stop], symbolize_names: true)
758
+ end
759
+ normalize_contract(raw: json)
760
+ rescue StandardError
761
+ EMPTY_CONTRACT.dup
762
+ end
763
+
764
+ private_class_method def self.deliverable_missing?(opts = {})
765
+ path = opts[:path].to_s
766
+ path.empty? || !File.file?(path) || File.size(path) <= 0
767
+ rescue StandardError
768
+ true
769
+ end
770
+
633
771
  private_class_method def self.may_finalize?(opts = {})
634
772
  return false if incomplete_final?(text: opts[:text], last_iter: false)
635
773
  return false if request_unsatisfied?(
@@ -2434,6 +2572,10 @@ module PWN
2434
2572
  Thread.current[:pwn_extinguished] = {}
2435
2573
  Thread.current[:pwn_same_payload] = Hash.new(0)
2436
2574
  Thread.current[:pwn_loop_t0] = Time.now unless nested
2575
+ unless nested
2576
+ Thread.current[:pwn_loop_active] = true
2577
+ Thread.current[:pwn_loop_deliverables] = nil
2578
+ end
2437
2579
  debug_progress(msg: "intent=#{intent} engine=#{engine}", debug: opts[:debug])
2438
2580
  expose_current_session(session_id: session_id)
2439
2581
  Mistakes.check_user_correction(request: request, session_id: session_id) if defined?(Mistakes)
@@ -2672,12 +2814,20 @@ module PWN
2672
2814
  # commit that as the answer; drop the empty assistant turn,
2673
2815
  # inject a one-shot nudge, and keep iterating.
2674
2816
  if calls.empty? && text.strip.empty?
2817
+ unsat = request_unsatisfied?(request: request, messages: messages)
2675
2818
  warn "[pwn-ai/loop] empty final from #{engine} on iter=#{i}; nudging" if local
2819
+ empty_nudge = if unsat
2820
+ 'Your previous reply was empty (no tool_calls and no content). ' \
2821
+ 'The original request is not evidenced yet. Emit NATIVE tool_calls NOW. ' \
2822
+ 'Do not write a final answer until that request is done or a tool returned failure evidence.'
2823
+ else
2824
+ 'Your previous reply was empty (no tool_calls and no content). ' \
2825
+ 'Either call a tool now, or write the final answer for the user as plain text. ' \
2826
+ 'Do not reply with an empty message.'
2827
+ end
2676
2828
  messages << {
2677
2829
  role: 'user',
2678
- content: 'Your previous reply was empty (no tool_calls and no content). ' \
2679
- 'Either call a tool now, or write the final answer for the user as plain text. ' \
2680
- 'Do not reply with an empty message.'
2830
+ content: empty_nudge
2681
2831
  }
2682
2832
  turn_fails['empty_final'] += 1
2683
2833
  debug_progress(msg: "bounce empty_final snippet=#{debug_snippet(text: text)}")
@@ -2830,6 +2980,10 @@ module PWN
2830
2980
  end
2831
2981
  raise
2832
2982
  ensure
2983
+ unless nested
2984
+ Thread.current[:pwn_loop_active] = nil
2985
+ Thread.current[:pwn_loop_deliverables] = nil
2986
+ end
2833
2987
  Thread.current[:pwn_loop_no_tools] = nil
2834
2988
  finish_debug_request!(
2835
2989
  iter: i,
data/lib/pwn/config.rb CHANGED
@@ -819,27 +819,12 @@ module PWN
819
819
  end
820
820
 
821
821
  DEFAULT_SKILLS_DIR = File.expand_path('../../etc/default_skills', __dir__)
822
- DEFAULT_SKILL_NAMES = %w[
823
- vulnerability-research-fundamentals
824
- deep-exploitation
825
- bug-bounty-hunting
826
- sast-code-scans
827
- reverse-engineering-binaries
828
- penetration-testing
829
- web-application-penetration-testing
830
- red-teaming
831
- hardware-and-firmware-testing
832
- social-engineering
833
- osint
834
- cwe
835
- capec
836
- att&ck
837
- ].freeze
838
822
 
839
823
  public_class_method def self.default_skill_names(opts = {})
840
- return DEFAULT_SKILL_NAMES.dup if opts.is_a?(Hash)
841
-
842
- DEFAULT_SKILL_NAMES.dup
824
+ root = opts[:source] || opts[:root] || default_skills_dir
825
+ names = []
826
+ each_skill_md(root: root) { |_path, rel_dir| names << rel_dir }
827
+ names.sort
843
828
  end
844
829
 
845
830
  public_class_method def self.default_skills_dir(opts = {})
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.704'
4
+ VERSION = '0.5.706'
5
5
  end
@@ -71,7 +71,7 @@ RSpec.describe 'PWN::Config / etc/*.EXAMPLE hygiene', :aggregate_failures do
71
71
  expect(PWN::Config).to respond_to(:install_default_skills)
72
72
  expect(PWN::Config).to respond_to(:default_skill_names)
73
73
  names = PWN::Config.default_skill_names
74
- expect(names).to eq %w[
74
+ sops = %w[
75
75
  vulnerability-research-fundamentals
76
76
  deep-exploitation
77
77
  bug-bounty-hunting
@@ -87,9 +87,16 @@ RSpec.describe 'PWN::Config / etc/*.EXAMPLE hygiene', :aggregate_failures do
87
87
  capec
88
88
  att&ck
89
89
  ]
90
+ expect(names).to include(*sops)
91
+ expect(names).to include('pwn/www/youtube')
92
+ expect(names).to include('pwn/plugins/google_workspace')
93
+ expect(names.none? { |n| n.include?('/references/') }).to eq(true)
94
+ expect(names).to eq(names.sort)
95
+ expect(names.length).to be > sops.length
96
+ sop_seeded = names.reject { |n| n == 'pwn' || n.start_with?('pwn/') }
90
97
  seeded = PWN::Config.install_default_skills(pwn_skills_path: dir)
91
- expect(seeded.map { |r| r[:name] }).to match_array(names)
92
- names.each do |name|
98
+ expect(seeded.map { |r| r[:name] }).to match_array(sop_seeded)
99
+ sops.each do |name|
93
100
  path = File.join(dir, name, 'SKILL.md')
94
101
  expect(File.file?(path)).to eq(true), path
95
102
  body = File.read(path)
@@ -157,4 +164,18 @@ RSpec.describe 'PWN::Config / etc/*.EXAMPLE hygiene', :aggregate_failures do
157
164
  PWN.const_set(:Skills, prev.freeze) if prev
158
165
  end
159
166
  end
167
+
168
+ it 'default_skill_names recursively lists SKILL.md dirs under the default_skills tree' do
169
+ Dir.mktmpdir('pwn_skill_names') do |dir|
170
+ FileUtils.mkdir_p(File.join(dir, 'research', 'nested-probe'))
171
+ File.write(File.join(dir, 'research', 'nested-probe', 'SKILL.md'), "# nested\n")
172
+ FileUtils.mkdir_p(File.join(dir, 'osint'))
173
+ File.write(File.join(dir, 'osint', 'SKILL.md'), "# osint\n")
174
+ FileUtils.mkdir_p(File.join(dir, 'osint', 'references'))
175
+ File.write(File.join(dir, 'osint', 'references', 'note.md'), "# note\n")
176
+ FileUtils.mkdir_p(File.join(dir, 'empty-dir'))
177
+ names = PWN::Config.default_skill_names(source: dir)
178
+ expect(names).to eq(%w[osint research/nested-probe])
179
+ end
180
+ end
160
181
  end
@@ -910,6 +910,113 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
910
910
  Thread.current[:pwn_loop_t0] = nil
911
911
  end
912
912
 
913
+ it 'keeps LLM-declared deliverable paths unsatisfied until those files exist' do
914
+ path = "/tmp/pwn-deliv-#{Process.pid}.bin"
915
+ FileUtils.rm_f(path)
916
+ Thread.current[:pwn_loop_deliverables] = [path]
917
+ req = 'Exhaustively analyze the docker app. 100% coverage. Store the PDF in /tmp/container_pentest-p4-PWN-122B.pdf'
918
+ recap = 'Good! Let me continue the penetration test with more aggressive testing now.'
919
+ msgs = [
920
+ { role: 'user', content: req },
921
+ {
922
+ role: 'assistant',
923
+ tool_calls: [{ function: { name: 'pwn_eval', arguments: '{"code":"File.write(\"/tmp/pentest_report.json\",\"x\")"}' } }]
924
+ },
925
+ {
926
+ role: 'tool',
927
+ name: 'pwn_eval',
928
+ content: '{"success":true,"result":{"stdout":"Report saved to /tmp/pentest_report.json"},"effect":"write"}'
929
+ },
930
+ {
931
+ role: 'assistant',
932
+ tool_calls: [{ function: { name: 'shell', arguments: '{"command":"curl -s http://127.0.0.1:5000/auth/login"}' } }]
933
+ },
934
+ {
935
+ role: 'tool',
936
+ name: 'shell',
937
+ content: '{"success":true,"result":{"stdout":"<form>"},"effect":"read"}'
938
+ },
939
+ { role: 'assistant', content: recap }
940
+ ]
941
+ expect(described_class.send(:request_unsatisfied?, request: req, messages: msgs)).to eq(true)
942
+ expect(described_class.send(:may_finalize?, request: req, messages: msgs, text: recap)).to eq(false)
943
+ File.write(path, 'any bytes')
944
+ expect(described_class.send(:request_unsatisfied?, request: req, messages: msgs)).to eq(false)
945
+ ensure
946
+ FileUtils.rm_f(path)
947
+ Thread.current[:pwn_loop_deliverables] = nil
948
+ end
949
+
950
+ it 'parses a full acceptance contract from one engine JSON hop' do
951
+ src = File.read(described_class.method(:run).source_location.first)
952
+ expect(src).not_to match(/NAMED_PATH_RX/)
953
+ expect(src).not_to include('%PDF')
954
+ expect(src).to match(/infer_deliverables|pwn_loop_deliverables/)
955
+ payload = {
956
+ paths: ['/tmp/out.json'],
957
+ min_seconds: 28_800,
958
+ skills: ['penetration-testing'],
959
+ proofs: ['/tmp/poc.sh'],
960
+ hosts: ['127.0.0.1']
961
+ }
962
+ allow(described_class).to receive(:call_engine).and_return(payload.to_json)
963
+ Thread.current[:pwn_loop_active] = true
964
+ Thread.current[:pwn_loop_deliverables] = nil
965
+ paths = described_class.send(:declared_deliverables, request: 'write a report somewhere')
966
+ expect(paths).to eq(['/tmp/out.json'])
967
+ contract = described_class.send(:declared_contract, request: 'write a report somewhere')
968
+ expect(contract[:min_seconds]).to eq(28_800)
969
+ expect(contract[:skills]).to eq(['penetration-testing'])
970
+ expect(contract[:proofs]).to eq(['/tmp/poc.sh'])
971
+ expect(contract[:hosts]).to eq(['127.0.0.1'])
972
+ expect(described_class).to have_received(:call_engine).once
973
+ ensure
974
+ Thread.current[:pwn_loop_active] = nil
975
+ Thread.current[:pwn_loop_deliverables] = nil
976
+ end
977
+
978
+ it 'verifies LLM-declared duration, skills, proofs, and hosts in the world' do
979
+ proof = "/tmp/pwn-proof-#{Process.pid}.txt"
980
+ FileUtils.rm_f(proof)
981
+ req = 'black-box test 127.0.0.1 using penetration-testing'
982
+ recap = 'done'
983
+ msgs = [
984
+ { role: 'user', content: req },
985
+ {
986
+ role: 'assistant',
987
+ tool_calls: [{ function: { name: 'pwn_eval', arguments: '{"code":"1"}' } }]
988
+ },
989
+ {
990
+ role: 'tool',
991
+ name: 'pwn_eval',
992
+ content: '{"success":true,"result":{"value":"1","stdout":"scanned 10.0.0.1"},"effect":"eval"}'
993
+ },
994
+ { role: 'assistant', content: recap }
995
+ ]
996
+ Thread.current[:pwn_loop_t0] = Time.now
997
+ Thread.current[:pwn_loop_deliverables] = {
998
+ paths: [],
999
+ min_seconds: 60,
1000
+ skills: ['no-such-bundled-skill'],
1001
+ proofs: [proof],
1002
+ hosts: ['127.0.0.1']
1003
+ }
1004
+ expect(described_class.send(:request_unsatisfied?, request: req, messages: msgs)).to eq(true)
1005
+ Thread.current[:pwn_loop_deliverables][:min_seconds] = 0
1006
+ expect(described_class.send(:request_unsatisfied?, request: req, messages: msgs)).to eq(true)
1007
+ Thread.current[:pwn_loop_deliverables][:skills] = []
1008
+ expect(described_class.send(:request_unsatisfied?, request: req, messages: msgs)).to eq(true)
1009
+ File.write(proof, 'poc')
1010
+ expect(described_class.send(:request_unsatisfied?, request: req, messages: msgs)).to eq(true)
1011
+ msgs[2][:content] = '{"success":true,"result":{"value":"1","stdout":"open 127.0.0.1:5000"},"effect":"eval"}'
1012
+ expect(described_class.send(:request_unsatisfied?, request: req, messages: msgs)).to eq(false)
1013
+ expect(described_class.send(:may_finalize?, request: req, messages: msgs, text: recap)).to eq(true)
1014
+ ensure
1015
+ FileUtils.rm_f(proof)
1016
+ Thread.current[:pwn_loop_t0] = nil
1017
+ Thread.current[:pwn_loop_deliverables] = nil
1018
+ end
1019
+
913
1020
  it 'forces tool_choice required on host-work before any tool result, for every engine' do
914
1021
  src = File.read(described_class.method(:run).source_location.first)
915
1022
  expect(src).to match(/tool_choice/)
@@ -309,6 +309,7 @@
309
309
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.update_skill Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.update_skill`: "}]}
310
310
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.verdict_for_score Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.verdict_for_score`: "}]}
311
311
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.weighted_judge_mean Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.weighted_judge_mean`: "}]}
312
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.abs_paths Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.abs_paths`: "}]}
312
313
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.active_engine Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.active_engine`: "}]}
313
314
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.agent_flag Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.agent_flag`: "}]}
314
315
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.answer_greeting Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.answer_greeting`: "}]}
@@ -335,8 +336,15 @@
335
336
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.debug_snippet Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.debug_snippet`: "}]}
336
337
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.debug_tool_io! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.debug_tool_io!`: "}]}
337
338
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.debug_tools_line Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.debug_tools_line`: "}]}
339
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.declared_contract Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.declared_contract`: "}]}
340
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.declared_contract_unsatisfied? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.declared_contract_unsatisfied?`: "}]}
341
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.declared_deliverables Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.declared_deliverables`: "}]}
342
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.declared_hosts_missing? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.declared_hosts_missing?`: "}]}
343
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.declared_min_seconds Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.declared_min_seconds`: "}]}
344
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.declared_skills_missing? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.declared_skills_missing?`: "}]}
338
345
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.default_interactive_toolsets Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.default_interactive_toolsets`: "}]}
339
346
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.degrade_text_only Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.degrade_text_only`: "}]}
347
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.deliverable_missing? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.deliverable_missing?`: "}]}
340
348
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.dispatch_fail_n Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.dispatch_fail_n`: "}]}
341
349
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.duration_unsatisfied? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.duration_unsatisfied?`: "}]}
342
350
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.emit_task_summary Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.emit_task_summary`: "}]}
@@ -349,6 +357,7 @@
349
357
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.guard_repeated_failure Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.guard_repeated_failure`: "}]}
350
358
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.help`: "}]}
351
359
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.incomplete_final? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.incomplete_final?`: "}]}
360
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.infer_deliverables Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.infer_deliverables`: "}]}
352
361
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.inject_task_focus! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.inject_task_focus!`: "}]}
353
362
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.local_engine? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.local_engine?`: "}]}
354
363
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.loud_debug_tui! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.loud_debug_tui!`: "}]}
@@ -361,12 +370,14 @@
361
370
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.mistake_ts Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.mistake_ts`: "}]}
362
371
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.needs_host_work? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.needs_host_work?`: "}]}
363
372
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.no_progress_result Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.no_progress_result`: "}]}
373
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.normalize_contract Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.normalize_contract`: "}]}
364
374
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.normalize_llm Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.normalize_llm`: Supported Method Parameters\n\nmsg = PWN::AI::Agent::Loop.normalize_llm(\n\nresponse: 'required - chat_with_tools response Hash from any provider'\n\n)\n"}]}
365
375
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.note_same_payload! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.note_same_payload!`: "}]}
366
376
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.ollama_wire_messages Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.ollama_wire_messages`: Supported Method Parameters\n\nwire = PWN::AI::Agent::Loop.ollama_wire_messages(\n\nmessages: 'required - in-memory OpenAI-ish messages (may have String args)'\n\n)\n\nReturns a deep-copied array safe for Ollama / Open WebUI ollama/api/chat:\n\nparses JSON-string function.arguments into Hash/Array objects\n\ncoerces nil assistant content to ” when tool_calls present (Open WebUI GenerateChatCompletionForm rejects content:null alone)\n\ndrops _native_content / _text_tool_coerced / thinking private keys\n\nstringifies Hash/Array message content (tool results) to JSON text\n\n"}]}
367
377
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.ollama_wire_tool_call Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.ollama_wire_tool_call`: "}]}
368
378
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.openai_wire_messages Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.openai_wire_messages`: Supported Method Parameters\n\nwire = PWN::AI::Agent::Loop.openai_wire_messages(\n\nmessages: 'required - in-memory OpenAI-ish messages (may have Hash args / internal keys)'\n\n)\n\nReturns a deep-copied array safe for OpenAI / xAI chat.completions:\n\ndrops _native_content / _text_tool_coerced / thinking private keys\n\nstringifies function.arguments maps\n\ncoerces Hash/non-string content to JSON/string (nil kept for assistant tool turns)\n\n"}]}
369
379
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.openai_wire_tool_call Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.openai_wire_tool_call`: "}]}
380
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.parse_contract Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.parse_contract`: "}]}
370
381
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.parse_tool_arguments Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.parse_tool_arguments`: "}]}
371
382
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.payload_sig Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.payload_sig`: "}]}
372
383
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.plan_first Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.plan_first`: "}]}
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.704
4
+ version: 0.5.706
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -1051,28 +1051,28 @@ dependencies:
1051
1051
  requirements:
1052
1052
  - - '='
1053
1053
  - !ruby/object:Gem::Version
1054
- version: 0.151.0
1054
+ version: 0.152.0
1055
1055
  type: :runtime
1056
1056
  prerelease: false
1057
1057
  version_requirements: !ruby/object:Gem::Requirement
1058
1058
  requirements:
1059
1059
  - - '='
1060
1060
  - !ruby/object:Gem::Version
1061
- version: 0.151.0
1061
+ version: 0.152.0
1062
1062
  - !ruby/object:Gem::Dependency
1063
1063
  name: selenium-webdriver
1064
1064
  requirement: !ruby/object:Gem::Requirement
1065
1065
  requirements:
1066
1066
  - - '='
1067
1067
  - !ruby/object:Gem::Version
1068
- version: 4.47.0
1068
+ version: 4.48.0
1069
1069
  type: :runtime
1070
1070
  prerelease: false
1071
1071
  version_requirements: !ruby/object:Gem::Requirement
1072
1072
  requirements:
1073
1073
  - - '='
1074
1074
  - !ruby/object:Gem::Version
1075
- version: 4.47.0
1075
+ version: 4.48.0
1076
1076
  - !ruby/object:Gem::Dependency
1077
1077
  name: slack-ruby-client
1078
1078
  requirement: !ruby/object:Gem::Requirement