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 +4 -4
- data/Gemfile +4 -4
- data/documentation/pwn-ai-Agent.md +4 -2
- data/lib/pwn/ai/anthropic.rb +73 -1
- data/lib/pwn/plugins/repl/ai_console.rb +80 -13
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/anthropic_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +68 -8
- data/third_party/pwn_rdoc.jsonl +6 -0
- metadata +10 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '088caeab6962a895018847c637332f41ca7033f69c7e8a547862d219c58b162c'
|
|
4
|
+
data.tar.gz: b0562e89e6cd08079728c7d5f0b2f640b975f482ed028c1cbe6b27a363361172
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
93
|
-
gem 'selenium-webdriver', '4.
|
|
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 |
|
|
119
|
-
|
|
|
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 |
|
data/lib/pwn/ai/anthropic.rb
CHANGED
|
@@ -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']
|
|
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 :
|
|
731
|
-
@
|
|
732
|
-
|
|
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 << "\
|
|
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 << "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
@@ -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 ' *
|
|
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
|
|
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
|
-
|
|
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
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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: []
|