pwn 0.5.752 → 0.5.755

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: 9f090409a234eeffe46292971f044bd3f6109da51d03e8fae5909d63aa625808
4
- data.tar.gz: '082fcd50fde34da2c0bea93a7292a4925fd017e2526e07d6fcf8c9b29bb5db8c'
3
+ metadata.gz: '088caeab6962a895018847c637332f41ca7033f69c7e8a547862d219c58b162c'
4
+ data.tar.gz: b0562e89e6cd08079728c7d5f0b2f640b975f482ed028c1cbe6b27a363361172
5
5
  SHA512:
6
- metadata.gz: 612c4b3a4d8f8311e231c2bbcceee5cd61280134157adc9f2132beb438ef65bbb698ed949aca56d19412414ed10c8540a1feab8327aaf14abcc71c33c2b23ccf
7
- data.tar.gz: '09b64fed3ccd1e064a28b4874005e73549c8fbd7d60017dd6c552cce7bd6b45577dfb9202d7aecc54aa13456a2cdc847ef021850d387139bfa064fdc88fbc7d5'
6
+ metadata.gz: 2730afce4cd70424a7fb927983bb417c1bb755846cd33113ee522f3b4d1a2e3a227251b7f3e5079b9bf3198224f8d36d5f31bed362acc37b6022fb0da03c417d
7
+ data.tar.gz: 9ac89ddaeb41583bc8b0db2a98faf9696ae78aae7f8ecc2689f4f8519071e1c284d172245aeb586de4a33c4ad7a81e4dc7ee16a7b849c23f4429e4ee9456e8ca
data/Gemfile CHANGED
@@ -20,7 +20,7 @@ gem 'base32', '0.3.4'
20
20
  gem 'bitcoin-ruby', '0.0.20'
21
21
  gem 'brakeman', '8.0.6'
22
22
  gem 'bson', '5.2.0'
23
- gem 'bundler', '>=4.0.21'
23
+ gem 'bundler', '>=4.0.22'
24
24
  gem 'bundler-audit', '>=0.9.3'
25
25
  gem 'bunny', '3.4.0'
26
26
  gem 'colorize', '1.1.0'
@@ -57,7 +57,7 @@ gem 'msfrpc-client', '1.1.2'
57
57
  gem 'netaddr', '2.0.6'
58
58
  gem 'net-ldap', '0.20.0'
59
59
  gem 'net-openvpn', '0.8.7'
60
- gem 'net-smtp', '0.5.1'
60
+ gem 'net-smtp', '0.5.2'
61
61
  gem 'nexpose', '7.3.0'
62
62
  gem 'nokogiri', '1.19.4'
63
63
  gem 'nokogiri-diff', '0.3.0'
@@ -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.5'
92
- gem 'selenium-devtools', '0.153.0'
93
- gem 'selenium-webdriver', '4.49.0'
92
+ gem 'selenium-devtools', '0.154.0'
93
+ gem 'selenium-webdriver', '4.50.0'
94
94
  gem 'slack-ruby-client', '3.2.0'
95
95
  gem 'socksify', '1.8.1'
96
96
  gem 'spreadsheet', '1.3.5'
@@ -115,8 +115,10 @@ changes only default values, not the configuration schema; no migration is neede
115
115
  | --- | --- |
116
116
  | Enter | Submit the mission |
117
117
  | Shift+Enter, or trailing `\\` + Enter | Insert a newline. Terminals that cannot distinguish Shift+Enter need `tmux set -s extended-keys on` or the backslash fallback. |
118
- | Up/Down | Move the completion highlight while the Command menu is visible (wrapping at either end). Otherwise recall requests from `~/.pwn/pwn_history`, including prior runs; Down past the newest restores the draft and cursor. Esc closes the menu to resume history recall. |
119
- | Ctrl+P / Ctrl+N | Alternative completion selection keys; recall history when no menu is visible. Tab still accepts the highlight. |
118
+ | Up/Down | In MISSION CONTROL, move the completion highlight while the Command menu is visible (wrapping at either end). Otherwise recall requests from `~/.pwn/pwn_history`, including prior runs; Down past the newest restores the draft and cursor. In SESSION, move one transcript row. Esc closes the menu to resume history recall. |
119
+ | Home/End | In MISSION CONTROL, move the composer cursor to the start or end of the line. In SESSION, jump to the top of the transcript or back to the live bottom. |
120
+ | Ctrl+X | Toggle the active pane between SESSION and MISSION CONTROL. The active title is reversed and marked `active`. Typing or Enter returns to MISSION CONTROL; Enter does not submit while SESSION is active. |
121
+ | Ctrl+P / Ctrl+N | Alternative completion selection keys. In MISSION CONTROL they recall history when no menu is visible. In SESSION they scroll one row. Tab still accepts the highlight. |
120
122
  | Tab | Accept the highlighted as-you-type parameter |
121
123
  | Ctrl+O or `/status` | Inspect full status/settings, including overflowing header text; Esc or Ctrl+O closes |
122
124
  | Ctrl+L | Clear only the Session pane, including while a request runs; preserve stored conversation, token totals, and draft |
@@ -575,7 +575,7 @@ module PWN
575
575
  {
576
576
  name: fn[:name] || fn['name'],
577
577
  description: fn[:description] || fn['description'],
578
- input_schema: fn[:parameters] || fn['parameters'] || { type: 'object', properties: {} }
578
+ input_schema: anthropic_input_schema(schema: fn[:parameters] || fn['parameters'])
579
579
  }
580
580
  end
581
581
  http_body[:tool_choice] = anth_tool_choice(choice: opts[:tool_choice]) if opts[:tool_choice]
@@ -598,6 +598,78 @@ module PWN
598
598
  raise e
599
599
  end
600
600
 
601
+ # Anthropic rejects oneOf, allOf, and anyOf at the root of input_schema.
602
+ # Return a deep copy with those root keys removed and branch properties merged.
603
+ # Nested combinators and the caller's schema stay unchanged.
604
+ private_class_method def self.anthropic_input_schema(opts = {})
605
+ schema = opts[:schema]
606
+ return { type: 'object', properties: {} } unless schema.is_a?(Hash)
607
+
608
+ copy = json_copy(value: schema)
609
+ notes = []
610
+ %w[anyOf oneOf allOf].each do |name|
611
+ key = anthropic_schema_key(schema: copy, name: name)
612
+ next unless key
613
+
614
+ Array(copy.delete(key)).each do |branch|
615
+ next unless branch.is_a?(Hash)
616
+
617
+ anthropic_schema_notes(branch: branch, notes: notes)
618
+ props = branch[:properties] || branch['properties']
619
+ next unless props.is_a?(Hash)
620
+
621
+ dest = copy.key?('properties') ? 'properties' : :properties
622
+ copy[dest] = {} unless copy[dest].is_a?(Hash)
623
+ props.each { |prop, spec| copy[dest][prop] = spec unless copy[dest].key?(prop) }
624
+ end
625
+ end
626
+ copy[:type] = 'object' unless copy.key?(:type) || copy.key?('type')
627
+ copy[:properties] = {} unless copy.key?(:properties) || copy.key?('properties')
628
+ unless notes.empty?
629
+ note = "One of these field sets is required: #{notes.uniq.join('; ')}."
630
+ copy[:description] = [copy[:description] || copy['description'], note].compact.reject { |part| part.to_s.empty? }.join(' ')
631
+ copy.delete('description')
632
+ end
633
+ copy
634
+ end
635
+
636
+ private_class_method def self.anthropic_schema_notes(opts = {})
637
+ branch = opts[:branch]
638
+ notes = opts[:notes]
639
+ return notes unless branch.is_a?(Hash) && notes.is_a?(Array)
640
+
641
+ required = branch[:required] || branch['required']
642
+ notes << required.join('+') if required.is_a?(Array) && required.any?
643
+ %w[anyOf oneOf allOf].each do |name|
644
+ key = anthropic_schema_key(schema: branch, name: name)
645
+ next unless key
646
+
647
+ Array(branch[key]).each { |child| anthropic_schema_notes(branch: child, notes: notes) }
648
+ end
649
+ notes
650
+ end
651
+
652
+ private_class_method def self.anthropic_schema_key(opts = {})
653
+ schema = opts[:schema]
654
+ name = opts[:name]
655
+ return name if schema.is_a?(Hash) && schema.key?(name)
656
+ return name.to_sym if schema.is_a?(Hash) && schema.key?(name.to_sym)
657
+
658
+ nil
659
+ end
660
+
661
+ private_class_method def self.json_copy(opts = {})
662
+ value = opts[:value]
663
+ case value
664
+ when Hash
665
+ value.each_with_object({}) { |(key, item), out| out[key] = json_copy(value: item) }
666
+ when Array
667
+ value.map { |item| json_copy(value: item) }
668
+ else
669
+ value.is_a?(String) ? value.dup : value
670
+ end
671
+ end
672
+
601
673
  # OpenAI messages[] -> [system_string, anthropic messages[]]
602
674
  private_class_method def self.oa_messages_to_anthropic(opts = {})
603
675
  messages = opts[:messages] ||= []
@@ -279,6 +279,7 @@ module PWN
279
279
  @editor = Editor.new
280
280
  @timeline = []
281
281
  @scroll = nil
282
+ @focus = :mission
282
283
  @event_count = @completed_tools = @unseen = 0
283
284
  @verbose = true
284
285
  @usage = AIConsoleUsage::Tracker.new
@@ -652,6 +653,28 @@ module PWN
652
653
  @menu = nil if @menu.empty?
653
654
  end
654
655
 
656
+ # Live view uses a nil scroll. Up from that view pins one row above the bottom.
657
+ def scroll_session(key)
658
+ bottom = [@total_rows.to_i - @page_size.to_i, 0].max
659
+ case key
660
+ when :up, "\u0010"
661
+ @scroll = [(@scroll || bottom) - 1, 0].max
662
+ when :down, "\u000e"
663
+ return if @scroll.nil?
664
+
665
+ @scroll += 1
666
+ if @scroll >= bottom
667
+ @scroll = nil
668
+ @unseen = 0
669
+ end
670
+ when :home
671
+ @scroll = bottom.zero? ? nil : 0
672
+ when :end
673
+ @scroll = nil
674
+ @unseen = 0
675
+ end
676
+ end
677
+
655
678
  def handle(key)
656
679
  return if key.nil?
657
680
  return handle_system_role(key) if @role_editor && (!@too_small || ["\e", "\u0003", "\u0004", :eof].include?(key))
@@ -681,10 +704,17 @@ module PWN
681
704
  if key == "\u0012"
682
705
  @menu = nil
683
706
  @paste = false
707
+ @focus = :mission
684
708
  @editor.search_history
685
709
  return
686
710
  end
687
711
 
712
+ if key == "\u0018"
713
+ @menu = nil
714
+ @focus = @focus == :session ? :mission : :session
715
+ return
716
+ end
717
+
688
718
  if key == "\t" && @menu
689
719
  accept_completion
690
720
  return
@@ -702,7 +732,9 @@ module PWN
702
732
  when :paste_end then @paste = false
703
733
  when :newline then @editor.insert("\n")
704
734
  when "\r", "\n"
705
- if @paste
735
+ if @focus == :session && !@paste
736
+ @focus = :mission
737
+ elsif @paste
706
738
  @editor.insert("\n")
707
739
  elsif @editor.text.end_with?('\\')
708
740
  @editor.edit("\b")
@@ -718,6 +750,8 @@ module PWN
718
750
  direction = [:up, "\u0010"].include?(key) ? :up : :down
719
751
  if @menu
720
752
  @menu_index = (@menu_index + (direction == :up ? -1 : 1)) % @menu.length
753
+ elsif @focus == :session
754
+ scroll_session(key)
721
755
  else
722
756
  @editor.recall(direction)
723
757
  end
@@ -727,11 +761,21 @@ module PWN
727
761
  @scroll = nil if @scroll >= [@total_rows.to_i - @page_size.to_i, 0].max
728
762
  @unseen = 0 unless @scroll
729
763
  when "\t" then accept_completion
730
- when :left, :right, :home, :end, :delete, "\b", "\u007f", "\u0001", "\u0005", "\u0015"
731
- @editor.edit(key)
732
- refresh_completion
764
+ when :home, :end
765
+ if @focus == :session
766
+ scroll_session(key)
767
+ else
768
+ @editor.edit(key)
769
+ refresh_completion
770
+ end
771
+ when :left, :right, :delete, "\b", "\u007f", "\u0001", "\u0005", "\u0015"
772
+ unless @focus == :session
773
+ @editor.edit(key)
774
+ refresh_completion
775
+ end
733
776
  else
734
777
  if key.is_a?(String) && key.ord >= 32
778
+ @focus = :mission
735
779
  @editor.insert(key)
736
780
  refresh_completion
737
781
  end
@@ -1045,6 +1089,13 @@ module PWN
1045
1089
  put(top, left + 2, fit(" #{title} ", box_width - 4), tone(:title)) unless title.empty?
1046
1090
  end
1047
1091
 
1092
+ def mark_active(top, left, box_width, title)
1093
+ return unless @screen.respond_to?(:attron)
1094
+
1095
+ text = fit(" #{title} ", box_width - 4)
1096
+ @screen.attron(Curses::A_REVERSE) { put(top, left + 2, text, tone(:title)) }
1097
+ end
1098
+
1048
1099
  def seed_request_history
1049
1100
  # REPL configures ~/.pwn/pwn_history and Pry loads it at startup.
1050
1101
  # Reuse that owner rather than loading twice or opening another writer.
@@ -1070,10 +1121,10 @@ module PWN
1070
1121
  role = cfg[:system_role_content].to_s.gsub(/\s+/, ' ').strip
1071
1122
  parts = []
1072
1123
  parts << "SYSTEM ROLE CONTENT: #{role}" unless role.empty?
1073
- parts << "\nTEMP: #{cfg[:temp]}" unless cfg[:temp].nil?
1074
- parts << " MAX TOKENS: #{cfg[:max_tokens]}" unless cfg[:max_tokens].nil?
1124
+ parts << "\nMAX TOKENS: #{cfg[:max_tokens]}" unless cfg[:max_tokens].nil?
1075
1125
  parts << " MAX PROMPT LENGTH: #{cfg[:max_prompt_length]}" unless cfg[:max_prompt_length].nil?
1076
- parts << "\nREASONING EFFORT: #{cfg[:reasoning_effort]}" unless cfg[:reasoning_effort].to_s.empty?
1126
+ parts << " REASONING EFFORT: #{cfg[:reasoning_effort]}" unless cfg[:reasoning_effort].to_s.empty?
1127
+ parts << " TEMP: #{cfg[:temp]}" unless cfg[:temp].nil?
1077
1128
  parts.join(' · ')
1078
1129
  rescue StandardError
1079
1130
  ''
@@ -1262,21 +1313,36 @@ module PWN
1262
1313
  @total_rows = rows.length
1263
1314
  @scroll = @scroll.clamp(0, [rows.length - @page_size, 0].max) if @scroll
1264
1315
  session_id = @pry.config.pwn_ai_session_id.to_s
1265
- box(header_height, 0, timeline_height, pane_width, "SESSION #{session_id}")
1316
+ session_title = "SESSION #{session_id}"
1317
+ session_title += ' · active' if @focus == :session
1318
+ box(header_height, 0, timeline_height, pane_width, session_title)
1319
+ mark_active(header_height, 0, pane_width, session_title) if @focus == :session
1266
1320
  start = @scroll || [rows.length - @page_size, 0].max
1267
1321
  rows.slice(start, @page_size).to_a.each_with_index { |(color, row), index| put(header_height + 1 + index, 2, row, color) }
1268
1322
  draw_sidebar(header_height, pane_width + 1, timeline_height) if sidebar.positive?
1269
1323
  compose_y = @height - compose_height - 1
1270
- box(compose_y, 0, compose_height, @width - 1, 'MISSION CONTROL')
1324
+ mission_title = @focus == :session ? 'MISSION CONTROL' : 'MISSION CONTROL · active'
1325
+ box(compose_y, 0, compose_height, @width - 1, mission_title)
1326
+ mark_active(compose_y, 0, @width - 1, mission_title) if @focus != :session
1271
1327
  draw_composer(compose_y)
1272
- hint = @menu ? '↑↓/^P/^N select · Tab accept · Esc close' : '↑↓ history · PgUp/PgDn scroll'
1328
+ hint = if @menu
1329
+ '↑↓/^P/^N select · Tab accept · Esc close'
1330
+ elsif @focus == :session
1331
+ '↑↓/Home/End scroll · ^X mission'
1332
+ else
1333
+ '↑↓ history · ^X session'
1334
+ end
1273
1335
  put(@height - 1, 1, "^C cancel ^D back · #{hint} · ^G swarm ^L clear ^R search ^O status · Enter send", tone(:footer))
1274
1336
  draw_menu if @menu
1275
1337
  draw_model_prompt if @model_prompt
1276
1338
  draw_details(engine, model) if @details
1277
1339
  draw_swarm if @workspace
1278
1340
  draw_system_role if @role_editor
1279
- @screen.setpos(*@cursor_position) if @role_editor || !(@details || @workspace)
1341
+ if @focus == :session && !(@details || @workspace || @role_editor)
1342
+ @screen.setpos(header_height + 1, 2)
1343
+ elsif @role_editor || !(@details || @workspace)
1344
+ @screen.setpos(*@cursor_position)
1345
+ end
1280
1346
  @screen.refresh
1281
1347
  end
1282
1348
 
@@ -1291,7 +1357,7 @@ module PWN
1291
1357
  first = [cursor_row - 2, 0].max
1292
1358
  put(top + 1, 2, '›', tone(:prompt)) if first.zero?
1293
1359
  rows.slice(first, 3).to_a.each_with_index { |row, index| put(top + 1 + index, 4, row, tone(:composer)) }
1294
- hint = @menu ? 'Tab accept · ↑↓/^P/^N select · Esc close' : '^G swarm · Shift+Enter newline · / commands · ^O status'
1360
+ hint = @menu ? 'Tab accept · ↑↓/^P/^N select · Esc close' : '^X pane · ^G swarm · Shift+Enter newline · / commands · ^O status'
1295
1361
  put(top + 4, 4, fit(hint, @width - 7), tone(:footer))
1296
1362
  @cursor_position = [top + 1 + cursor_row - first, [4 + width(prefix.last), @width - 3].min]
1297
1363
  end
@@ -1557,7 +1623,8 @@ module PWN
1557
1623
  public_class_method def self.help
1558
1624
  puts "USAGE:
1559
1625
  # Ctrl+L clears only the session pane, including while busy.
1560
- # Up/Down select in menus, otherwise recall ~/.pwn/pwn_history; Ctrl+R searches history.
1626
+ # Ctrl+X switches the active pane between SESSION and MISSION CONTROL.
1627
+ # Up/Down select in menus, recall ~/.pwn/pwn_history in the mission pane, and scroll the session pane when it is active.
1561
1628
  # Search: Enter accepts without sending; Esc restores the draft.
1562
1629
  # Launch the single-owner curses console; non-terminals return unavailable.
1563
1630
  #{self}.run(
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.752'
4
+ VERSION = '0.5.755'
5
5
  end
@@ -58,4 +58,52 @@ describe PWN::AI::Anthropic do
58
58
  expect(src).to include('PromptCache.enabled?')
59
59
  end
60
60
  end
61
+
62
+ it 'drops root combinators from Anthropic input_schema without mutating registry schemas' do
63
+ allow(PWN::Env).to receive(:[]).with(:ai).and_return(
64
+ anthropic: { key: 'test-key', model: 'claude-test', temp: 1, max_tokens: 100 }
65
+ )
66
+ allow(PWN::AI::Agent::PromptCache).to receive(:enabled?).and_return(false) if defined?(PWN::AI::Agent::PromptCache)
67
+ tools = PWN::AI::Agent::Registry.definitions(core_only: true)
68
+ artifact = tools.find { |tool| tool.dig(:function, :name) == 'artifact_read' }
69
+ original = artifact[:function][:parameters]
70
+ nested = {
71
+ type: 'object',
72
+ properties: {
73
+ item: { anyOf: [{ type: 'string' }, { type: 'object', properties: { handle: { type: 'string' } } }] }
74
+ }
75
+ }
76
+ union = {
77
+ 'anyOf' => [
78
+ { 'type' => 'object', 'properties' => { 'handle' => { 'type' => 'string' } }, 'required' => ['handle'] },
79
+ { 'type' => 'object', 'properties' => { 'path' => { 'type' => 'string' } }, 'required' => ['path'] }
80
+ ]
81
+ }
82
+ sent = tools + [
83
+ { type: 'function', function: { name: 'nested_union', description: 'keep nested', parameters: nested } },
84
+ { type: 'function', function: { name: 'root_union', description: 'flatten', parameters: union } }
85
+ ]
86
+ captured = nil
87
+ allow(described_class).to receive(:anthropic_rest_call) do |opts|
88
+ captured = opts[:http_body]
89
+ '{"id":"msg_schema","type":"message","role":"assistant","content":[{"type":"text","text":"ok"}],"stop_reason":"end_turn"}'
90
+ end
91
+
92
+ described_class.chat_with_tools(messages: [{ role: 'user', content: 'hello' }], tools: sent, model: 'claude-test')
93
+
94
+ expect(original).to have_key(:anyOf)
95
+ wire = captured[:tools]
96
+ expect(wire.map { |tool| tool[:name] }[10]).to eq('artifact_read')
97
+ wire.each do |tool|
98
+ schema = tool[:input_schema]
99
+ expect(schema.keys.map(&:to_s)).not_to include('anyOf', 'oneOf', 'allOf')
100
+ end
101
+ read_schema = wire[10][:input_schema]
102
+ expect(read_schema[:properties].keys.map(&:to_s)).to include('handle', 'path', 'ref')
103
+ expect(read_schema[:description]).to include('handle', 'path', 'ref')
104
+ expect(wire.find { |tool| tool[:name] == 'nested_union' }[:input_schema][:properties][:item]).to have_key(:anyOf)
105
+ flat = wire.find { |tool| tool[:name] == 'root_union' }[:input_schema]
106
+ expect(flat[:properties].keys.map(&:to_s)).to include('handle', 'path')
107
+ expect(flat[:description]).to include('handle', 'path')
108
+ end
61
109
  end
@@ -535,10 +535,10 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
535
535
  expect(console.instance_variable_get(:@header_text_column)).to eq(2)
536
536
  end
537
537
  allow(curses).to receive_messages(cols: 120, lines: 26)
538
- PWN::Env[:ai][:ollama][:system_role_content] = 'operator ' * 110
538
+ PWN::Env[:ai][:ollama][:system_role_content] = 'operator ' * 200
539
539
  console.draw
540
540
  expect(console.instance_variable_get(:@header_text_column)).to eq(2)
541
- expect(console.header_lines('ollama', 'fixture', 115).length).to be <= 13
541
+ expect(console.header_lines('ollama', 'fixture', 115).length).to be > 16
542
542
  expect(PWN::Banner).to have_received(:mini_cells).once
543
543
  expect([console.instance_variable_get(:@editor).text, console.instance_variable_get(:@editor).cursor]).to eq(['retained draft', 4])
544
544
  end
@@ -719,13 +719,15 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
719
719
  expect(actual).to eq(lines)
720
720
  expect(header.select { |entry| entry[3] == console.tone(:category) }.map { |entry| entry[2] }.join).to include('SYSTEM ROLE CONTENT:', 'MAX PROMPT LENGTH:', 'REASONING EFFORT:')
721
721
  expect(header.select { |entry| entry[3] == console.tone(:header) }.map { |entry| entry[2] }.join).to include('operator', '1024', 'high')
722
- if columns == 48
723
- labels = header.select { |entry| entry[3] == console.tone(:category) }.map { |entry| entry[2] }
724
- expect(labels.join).to include('MAX PROMPT LENGTH:')
725
- expect(labels).not_to include(a_string_including('MAX PROMPT LENGTH:'))
726
- end
727
722
  expect(console.instance_variable_get(:@page_size)).to be >= 1
728
723
  end
724
+ console.header_lines('ollama', 'fixture', 35)
725
+ labels = []
726
+ console.instance_variable_get(:@header_spans).each do |row|
727
+ row.each { |role, text| labels << text if role == :category }
728
+ end
729
+ expect(labels.join).to include('MAX PROMPT LENGTH:')
730
+ expect(labels).not_to include(a_string_including('MAX PROMPT LENGTH:'))
729
731
  console.handle("\u000f")
730
732
  console.draw
731
733
  console.handle(:end)
@@ -945,6 +947,62 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
945
947
  expect(console.instance_variable_get(:@scroll)).to be_nil
946
948
  end
947
949
 
950
+ it 'toggles the active pane so session arrows, Home and End scroll without changing the draft' do
951
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
952
+ editor = console.instance_variable_get(:@editor)
953
+ editor.place('kept draft', 4)
954
+ console.instance_variable_set(:@total_rows, 100)
955
+ console.instance_variable_set(:@page_size, 20)
956
+ console.instance_variable_set(:@unseen, 3)
957
+ console.handle(:up)
958
+ expect(editor.text).to eq('kept draft')
959
+ expect(console.instance_variable_get(:@scroll)).to be_nil
960
+ console.handle("\u0018")
961
+ expect(console.instance_variable_get(:@focus)).to eq(:session)
962
+ console.handle(:up)
963
+ expect(console.instance_variable_get(:@scroll)).to eq(79)
964
+ console.handle("\u0010")
965
+ expect(console.instance_variable_get(:@scroll)).to eq(78)
966
+ console.handle(:home)
967
+ expect(console.instance_variable_get(:@scroll)).to eq(0)
968
+ console.handle(:end)
969
+ expect(console.instance_variable_get(:@scroll)).to be_nil
970
+ expect(console.instance_variable_get(:@unseen)).to eq(0)
971
+ console.handle(:up)
972
+ console.handle(:down)
973
+ expect(console.instance_variable_get(:@scroll)).to be_nil
974
+ expect([editor.text, editor.cursor]).to eq(['kept draft', 4])
975
+ console.handle("\n")
976
+ expect(console.instance_variable_get(:@focus)).to eq(:mission)
977
+ expect(editor.text).to eq('kept draft')
978
+ console.handle("\u0018")
979
+ console.handle('x')
980
+ expect(console.instance_variable_get(:@focus)).to eq(:mission)
981
+ expect(editor.text).to eq('keptx draft')
982
+ console.handle(:up)
983
+ expect(console.instance_variable_get(:@scroll)).to be_nil
984
+ expect(console.instance_variable_get(:@focus)).to eq(:mission)
985
+ end
986
+
987
+ it 'marks the active pane and parks the cursor in the session when that pane is selected' do
988
+ screen = double(erase: nil, refresh: nil, setpos: nil, addstr: nil, attron: nil)
989
+ allow(screen).to receive(:attron).and_yield
990
+ curses = double(lines: 36, cols: 120)
991
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: curses, getch: nil)
992
+ console.instance_variable_set(:@screen, screen)
993
+ painted = []
994
+ allow(console).to receive(:put) { |*args| painted << args }
995
+ console.draw
996
+ expect(painted.map { |row| row[2] }.join).to include('MISSION CONTROL · active')
997
+ expect(painted.map { |row| row[2] }.join).not_to include('SESSION · active')
998
+ expect(screen).to have_received(:attron).with(Curses::A_REVERSE).at_least(:once)
999
+ console.handle("\u0018")
1000
+ painted.clear
1001
+ console.draw
1002
+ expect(painted.map { |row| row[2] }.join).to include('SESSION · active')
1003
+ expect(screen).to have_received(:setpos).with(a_kind_of(Integer), 2)
1004
+ end
1005
+
948
1006
  it 'wraps at words without discarding code whitespace or splitting graphemes' do
949
1007
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
950
1008
  expect(console.wrap('some evidence', 12)).to eq(['some ', 'evidence'])
@@ -1238,7 +1296,9 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
1238
1296
  if rendered_header.include?('worker-effort')
1239
1297
  expect(rendered_header).to include('worker settings role', '0.42', '4321', '8765')
1240
1298
  expect(painted).to include([a_kind_of(Integer), a_kind_of(Integer), 'MODEL:', 7])
1241
- expect(painted).to include([a_kind_of(Integer), a_kind_of(Integer), ' worker-effort', 6])
1299
+ value = painted.find { |_y, _x, text, color| color == 6 && text.to_s.include?('worker-effort') }
1300
+ expect(value).not_to be_nil
1301
+ expect(value[2]).not_to include('REASONING EFFORT:')
1242
1302
  "\u0004"
1243
1303
  else
1244
1304
  Thread.pass
@@ -1058,13 +1058,17 @@
1058
1058
  {"messages":[{"role":"user","content":"PWN::AI::Agent::VulnGen.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::VulnGen.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
1059
1059
  {"messages":[{"role":"user","content":"PWN::AI::Agent::VulnGen.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::VulnGen.help`: "}]}
1060
1060
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.anth_tool_choice Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.anth_tool_choice`: "}]}
1061
+ {"messages":[{"role":"user","content":"PWN::AI::Anthropic.anthropic_input_schema Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.anthropic_input_schema`: "}]}
1061
1062
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.anthropic_resp_to_oa Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.anthropic_resp_to_oa`: "}]}
1062
1063
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.anthropic_rest_call Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.anthropic_rest_call`: Supported Method Parameters\n\nanthropic_rest_call(\n\ntoken: 'required - anthropic api key',\nhttp_method: 'optional HTTP method (defaults to GET)',\nbase_uri: 'optional base anthropic api URI (defaults to https://api.anthropic.com/v1)',\nrest_call: 'required rest call to make per the schema',\nparams: 'optional params passed in the URI or HTTP Headers',\nhttp_body: 'optional HTTP body sent in HTTP methods that support it e.g. POST',\ntimeout: 'optional timeout in seconds (defaults to 900)',\nspinner: 'optional - display spinner (defaults to false)'\n\n)\n"}]}
1064
+ {"messages":[{"role":"user","content":"PWN::AI::Anthropic.anthropic_schema_key Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.anthropic_schema_key`: "}]}
1065
+ {"messages":[{"role":"user","content":"PWN::AI::Anthropic.anthropic_schema_notes Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.anthropic_schema_notes`: "}]}
1063
1066
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.authors Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
1064
1067
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.chat Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.chat`: Supported Method Parameters\n\nresponse = PWN::AI::Anthropic.chat(\n\nrequest: 'required - message to Anthropic',\nmodel: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:anthropic][:model])',\ntemp: 'optional - creative response float (defaults to PWN::Env[:ai][:anthropic][:temp])',\nsystem_role_content: 'optional - context to set up the model behavior for conversation (Default: PWN::Env[:ai][:anthropic][:system_role_content])',\nresponse_history: 'optional - pass response back in to have a conversation',\nspeak_answer: 'optional speak answer using PWN::Plugins::Voice.text_to_speech (Default: nil)',\ntimeout: 'optional timeout in seconds (defaults to 900)',\nspinner: 'optional - display spinner (defaults to false)'\n\n)\n"}]}
1065
1068
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.chat_with_tools Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.chat_with_tools`: Supported Method Parameters\n\nresponse = PWN::AI::Anthropic.chat_with_tools(\n\nmessages: 'required - OpenAI-format messages array (system/user/assistant/tool)',\ntools: 'optional - OpenAI tools array [{type:\"function\", function:{...}}]',\ntool_choice: 'optional - \"auto\" | \"none\" | \"required\" | {type:\"function\", function:{name:..}}',\nmodel: 'optional - overrides PWN::Env[:ai][:anthropic][:model]',\ntemp: 'optional - temperature (defaults to PWN::Env[:ai][:anthropic][:temp] || 1)',\nmax_tokens: 'optional - defaults to PWN::Env[:ai][:anthropic][:max_tokens] || 128_000',\ntimeout: 'optional - seconds (default 900)',\nspinner: 'optional - display spinner (default false)'\n\n)\n"}]}
1066
1069
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.get_models Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.get_models`: Supported Method Parameters\n\nmodels = PWN::AI::Anthropic.get_models\n"}]}
1067
1070
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.help Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.help`: "}]}
1071
+ {"messages":[{"role":"user","content":"PWN::AI::Anthropic.json_copy Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.json_copy`: "}]}
1068
1072
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.jwt_exp Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.jwt_exp`: "}]}
1069
1073
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.oa_messages_to_anthropic Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.oa_messages_to_anthropic`: "}]}
1070
1074
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.oauth_token_expiring? Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.oauth_token_expiring?`: "}]}
@@ -3320,6 +3324,7 @@
3320
3324
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_system_role Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_system_role`: "}]}
3321
3325
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.header_layout Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.header_layout`: "}]}
3322
3326
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.header_lines Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.header_lines`: "}]}
3327
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.mark_active Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.mark_active`: "}]}
3323
3328
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.move_role_cursor Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.move_role_cursor`: "}]}
3324
3329
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.open_swarm Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.open_swarm`: "}]}
3325
3330
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.open_system_role Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.open_system_role`: "}]}
@@ -3332,6 +3337,7 @@
3332
3337
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.restore_streams Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.restore_streams`: "}]}
3333
3338
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.run Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.run`: "}]}
3334
3339
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.run_swarm Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.run_swarm`: "}]}
3340
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.scroll_session Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.scroll_session`: "}]}
3335
3341
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.scroll_status Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.scroll_status`: "}]}
3336
3342
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.seed_request_history Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.seed_request_history`: "}]}
3337
3343
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.send_tool_input Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.send_tool_input`: "}]}
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.752
4
+ version: 0.5.755
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -141,14 +141,14 @@ dependencies:
141
141
  requirements:
142
142
  - - ">="
143
143
  - !ruby/object:Gem::Version
144
- version: 4.0.21
144
+ version: 4.0.22
145
145
  type: :development
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
- version: 4.0.21
151
+ version: 4.0.22
152
152
  - !ruby/object:Gem::Dependency
153
153
  name: bundler-audit
154
154
  requirement: !ruby/object:Gem::Requirement
@@ -617,14 +617,14 @@ dependencies:
617
617
  requirements:
618
618
  - - '='
619
619
  - !ruby/object:Gem::Version
620
- version: 0.5.1
620
+ version: 0.5.2
621
621
  type: :runtime
622
622
  prerelease: false
623
623
  version_requirements: !ruby/object:Gem::Requirement
624
624
  requirements:
625
625
  - - '='
626
626
  - !ruby/object:Gem::Version
627
- version: 0.5.1
627
+ version: 0.5.2
628
628
  - !ruby/object:Gem::Dependency
629
629
  name: nexpose
630
630
  requirement: !ruby/object:Gem::Requirement
@@ -1051,28 +1051,28 @@ dependencies:
1051
1051
  requirements:
1052
1052
  - - '='
1053
1053
  - !ruby/object:Gem::Version
1054
- version: 0.153.0
1054
+ version: 0.154.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.153.0
1061
+ version: 0.154.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.49.0
1068
+ version: 4.50.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.49.0
1075
+ version: 4.50.0
1076
1076
  - !ruby/object:Gem::Dependency
1077
1077
  name: slack-ruby-client
1078
1078
  requirement: !ruby/object:Gem::Requirement
@@ -7228,7 +7228,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
7228
7228
  - !ruby/object:Gem::Version
7229
7229
  version: '0'
7230
7230
  requirements: []
7231
- rubygems_version: 4.0.21
7231
+ rubygems_version: 4.0.22
7232
7232
  specification_version: 4
7233
7233
  summary: Automated Security Testing for CI/CD Pipelines & Beyond
7234
7234
  test_files: []