pwn 0.5.705 → 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: a46dbcdcfe0fb82880e81cbe531199c9cbcba65da71abf609b392afbfab55356
4
- data.tar.gz: 49ff3675f07a0fd8f81ce86230ee7651316c772289ec9138a14862a92393f7e5
3
+ metadata.gz: f9233042c1c9518d8cfdcbfe6499eb521cb0348384554e03ab6641abdda253b9
4
+ data.tar.gz: 51b81fd36f292bc54e762d4bb47f5641c9b263addf9f37997aaa4c2f6a00c49f
5
5
  SHA512:
6
- metadata.gz: 14ffb11e8613dcc5d24830514d4efd2ddaf7c584df93671d41843bb42c6d927d62af7703da63e54e8b4fe7d2d0bf91dc76c140b404bced65a87b57b78adfd4a5
7
- data.tar.gz: abf7797752f082aa8d032fea627a72ceb81e2859672679f9f05898ef68d7ee4463184be9dc94c1ba45c2cada205e859566185d75ce40bac24bd9d4662ec48b4b
6
+ metadata.gz: 370d8b9305f7dcd2a692061b6708feca282546109533131d244cd97d207d1b216afac16baf44847a8a098c9847467208dd30e81084729f79ee7e3c16c050ae32
7
+ data.tar.gz: 97725299f78baef12d73614b7258ebf5cc3664316c82d2bf53d6a82e3d9e0e0d2542a3143cbc686fc75b46b6883fb8edc0174cfd38ce74b88a0c908ab4864642
@@ -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/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.705'
4
+ VERSION = '0.5.706'
5
5
  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.705
4
+ version: 0.5.706
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.