pwn 0.5.758 → 0.5.759

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: 004ad4f77955698492a48591db3dad3f54d141fb94ddb7952211e86dc4945526
4
- data.tar.gz: 5c76e0a2f71fdc1afd13f8af828c2027cb8183f3e73612d81f220b864b6df636
3
+ metadata.gz: 1ef7228011e10bd0573cab148f192e8ef963608a002d4e3d833fcd2e380fd973
4
+ data.tar.gz: 8f238e16e7d0d521deb4b4abdad45fed2d15fb82d92bcc8448b0877bc23b8783
5
5
  SHA512:
6
- metadata.gz: ffe44c731211488e5d07938e29dc1c503307801de30d0c2effc55d139d0f00b0a40a7deeda375f05b581ea88e379b9b82f031b589236bd5a08edd590078b3511
7
- data.tar.gz: 9c8275fd41e044aed262bcbe7fcf9fe00694b43227030ba1ce90d907c6e68b54efa53dacd3c3cb712890acc159005f5ecd4c9cf1caf065de8032cfc4123041de
6
+ metadata.gz: d6eb621a41250c5332e00f941c8b6b7444f796fcb30c009d774d7a0814f3c268809152275f2c63f7f7076450c75bbe662c0248ec6b21abb9730b44b097fdb139
7
+ data.tar.gz: 8ea184fdfa2018bc59f00aef8585d190907236a3d71df302c21db3eb025813111cd5a7bb289513c5a7f51a816f1c7e094f5b6c0bf420ff50d9a7a27a11ffbc65
data/Gemfile CHANGED
@@ -68,7 +68,7 @@ gem 'ostruct', '0.6.3'
68
68
  gem 'packetfu', '2.0.0'
69
69
  gem 'packetgen', '4.1.1'
70
70
  gem 'pdf-reader', '2.16.0'
71
- gem 'pg', '1.6.3'
71
+ gem 'pg', '1.7.0'
72
72
  gem 'pry', '0.16.0'
73
73
  gem 'pry-doc', '1.7.0'
74
74
  gem 'rake', '13.4.2'
@@ -1923,12 +1923,16 @@ module PWN
1923
1923
 
1924
1924
  content = msg[:content]
1925
1925
  tool_calls = Array(msg[:tool_calls])
1926
+ promoted = false
1926
1927
  # Local/thinking models (Ollama Qwen3, DeepSeek-R1, etc.) sometimes
1927
1928
  # return only :thinking with empty :content and no tool_calls. Promote
1928
1929
  # thinking so the agent does not print a blank final answer.
1929
1930
  if content.to_s.strip.empty? && tool_calls.empty?
1930
1931
  thinking = msg[:thinking].to_s
1931
- content = thinking unless thinking.strip.empty?
1932
+ unless thinking.strip.empty?
1933
+ content = thinking
1934
+ promoted = true
1935
+ end
1932
1936
  end
1933
1937
 
1934
1938
  out = {
@@ -1953,6 +1957,7 @@ module PWN
1953
1957
  out[:_native_content] = msg[:_native_content] if msg[:_native_content]
1954
1958
  thinking = msg[:thinking] || msg[:reasoning_content]
1955
1959
  out[:thinking] = thinking if thinking.to_s.strip != ''
1960
+ out[:_thinking_promoted] = true if promoted
1956
1961
  # Local/abliterated models sometimes emit shell(...) as plain content
1957
1962
  # with empty tool_calls. Coerce registered call-shaped text into
1958
1963
  # structured tool_calls so Loop dispatches instead of FINAL-answering.
@@ -2297,6 +2302,17 @@ module PWN
2297
2302
  nil
2298
2303
  end
2299
2304
 
2305
+ private_class_method def self.emit_thinking!(opts = {})
2306
+ text = opts[:thinking].to_s.strip
2307
+ Thread.current[:pwn_last_thinking] = text
2308
+ Thread.current[:pwn_thinking_promoted] = opts[:promoted] ? true : false
2309
+ return if text.empty?
2310
+
2311
+ opts[:on_tool]&.call('thinking', text, '')
2312
+ rescue StandardError
2313
+ nil
2314
+ end
2315
+
2300
2316
  # Pre-dispatch: one high-level brief for the whole upcoming tool
2301
2317
  # collection (pwn-ai → task is one-to-many with pwn-ai → <tool>).
2302
2318
  # Prefer opts[:tools] = [{name:, args:}, ...]; falls back to single name.
@@ -2304,18 +2320,13 @@ module PWN
2304
2320
  state = opts[:state]
2305
2321
  return nil unless state && defined?(TaskSummarizer) && TaskSummarizer.enabled?
2306
2322
 
2307
- thinking = opts[:thinking].to_s.strip
2308
- line = if thinking.empty?
2309
- TaskSummarizer.about_to(
2310
- name: opts[:name],
2311
- args: opts[:args],
2312
- state: state,
2313
- request: opts[:request],
2314
- tools: opts[:tools]
2315
- )
2316
- else
2317
- thinking
2318
- end
2323
+ line = TaskSummarizer.about_to(
2324
+ name: opts[:name],
2325
+ args: opts[:args],
2326
+ state: state,
2327
+ request: opts[:request],
2328
+ tools: opts[:tools]
2329
+ )
2319
2330
  # about_to returns nil when the brief is a duplicate of the last one
2320
2331
  emit_task_summary(line: line, on_tool: opts[:on_tool]) if line
2321
2332
  line
@@ -3409,6 +3420,11 @@ module PWN
3409
3420
 
3410
3421
  calls = Array(msg[:tool_calls])
3411
3422
  text = msg[:content].to_s
3423
+ emit_thinking!(
3424
+ thinking: msg[:thinking] || msg[:reasoning_content],
3425
+ promoted: msg[:_thinking_promoted],
3426
+ on_tool: on_tool
3427
+ )
3412
3428
 
3413
3429
  # Belt-and-suspenders: plain-text shell(...) / tool forms from local
3414
3430
  # models under weak TEMPLATE {{ .Prompt }} become real tool_calls.
@@ -3525,7 +3541,6 @@ module PWN
3525
3541
  }
3526
3542
  end,
3527
3543
  request: request,
3528
- thinking: msg[:thinking] || msg[:reasoning_content],
3529
3544
  on_tool: on_tool
3530
3545
  )
3531
3546
 
@@ -266,7 +266,7 @@ module PWN
266
266
 
267
267
  # Own the terminal, event pump and the lifetime of exactly one request.
268
268
  class Console # rubocop:disable Metrics/ClassLength -- cohesive single-owner terminal state machine
269
- COLORS = { operator: :operator, task: :task, tool: :tool, result: :result, assistant: :assistant, notice: :notice, warning: :warning }.freeze
269
+ COLORS = { operator: :operator, task: :task, thinking: :notice, tool: :tool, result: :result, assistant: :assistant, notice: :notice, warning: :warning }.freeze
270
270
  PALETTE = %w[cyan green yellow red white blue magenta black].freeze
271
271
 
272
272
  def initialize(pry:, input:, curses:, getch:)
@@ -480,6 +480,8 @@ module PWN
480
480
  on_tool = lambda do |name, args, result|
481
481
  if name.to_s == 'task'
482
482
  @events << [:task, args.to_s]
483
+ elsif name.to_s == 'thinking'
484
+ @events << [:thinking, args.to_s]
483
485
  else
484
486
  @events << [:tool, "#{name}\n#{args.is_a?(String) ? args : args.inspect}"]
485
487
  @events << [:result, result.to_s]
@@ -503,7 +505,8 @@ module PWN
503
505
  enabled_toolsets: PWN::Env.dig(:ai, :agent, :toolsets),
504
506
  on_tool: on_tool, steering: control, debug: @pry.config.pwn_ai_debug, debug_tee: @output
505
507
  )
506
- @events << [:assistant, final.to_s]
508
+ promoted = Thread.current[:pwn_thinking_promoted] && final.to_s.strip == Thread.current[:pwn_last_thinking].to_s.strip
509
+ @events << [:assistant, final.to_s] unless promoted
507
510
  end
508
511
  rescue Control::Stopped => e
509
512
  @events << [:warning, e.message]
@@ -1652,6 +1655,7 @@ module PWN
1652
1655
  puts "USAGE:
1653
1656
  # Ctrl+L clears only the session pane, including while busy.
1654
1657
  # Ctrl+T toggles the active pane between SESSION and MISSION CONTROL. Ctrl+S opens swarm.
1658
+ # Thinking from a model that returns it is a THINKING row in the session pane, above the answer.
1655
1659
  # Up/Down select in menus, recall ~/.pwn/pwn_history in the mission pane, and scroll the session pane when it is active.
1656
1660
  # Search: Enter accepts without sending; Esc restores the draft.
1657
1661
  # Launch the single-owner curses console; non-terminals return unavailable.
@@ -279,6 +279,10 @@ module PWN
279
279
 
280
280
  def tool_callback(job, opts)
281
281
  lambda do |name, args, result|
282
+ if name.to_s == 'thinking'
283
+ opts[:on_event]&.call(:thinking, args.to_s)
284
+ return
285
+ end
282
286
  update(job, job[:state], last_tool: name)
283
287
  opts[:on_event]&.call(:tool, "#{job[:agent]} [#{job[:id]}] #{name}\n#{args.inspect}")
284
288
  opts[:on_event]&.call(:result, result.to_s)
@@ -561,6 +561,19 @@ module PWN
561
561
  mirror.call("#{header}\n#{body_out}")
562
562
  next
563
563
  end
564
+ if name.to_s == 'thinking'
565
+ body = args.is_a?(String) ? args.to_s : args.inspect
566
+ timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
567
+ header = "[ #{timestamp} → pwn-ai → thinking ]"
568
+ puts "\e[36m#{header}\e[0m"
569
+ body_out = +''
570
+ body.to_s.each_line do |ln|
571
+ puts "\e[36m #{ln.rstrip}\e[0m"
572
+ body_out << " #{ln.rstrip}\n"
573
+ end
574
+ mirror.call("#{header}\n#{body_out}")
575
+ next
576
+ end
564
577
 
565
578
  argv = args.is_a?(String) ? args.to_s : args.inspect
566
579
  timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
@@ -592,9 +605,12 @@ module PWN
592
605
  debug_tee: $stdout
593
606
  )
594
607
  $stdout.flush
595
- puts "\n\e[32m#{final}\e[0m\n\n"
596
- $stdout.flush
597
- PWN::Plugins::Log.mirror_tui!(msg: "\n#{final}\n\n") if pi.config.pwn_ai_debug && defined?(PWN::Plugins::Log)
608
+ promoted = Thread.current[:pwn_thinking_promoted] && final.to_s.strip == Thread.current[:pwn_last_thinking].to_s.strip
609
+ unless promoted
610
+ puts "\n\e[32m#{final}\e[0m\n\n"
611
+ $stdout.flush
612
+ PWN::Plugins::Log.mirror_tui!(msg: "\n#{final}\n\n") if pi.config.pwn_ai_debug && defined?(PWN::Plugins::Log)
613
+ end
598
614
  if pi.config.pwn_ai_debug && sess_id && PWN.const_defined?(:Sessions)
599
615
  PWN::Plugins::Log.progress(
600
616
  msg: "session=#{sess_id}",
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.758'
4
+ VERSION = '0.5.759'
5
5
  end
@@ -2123,36 +2123,33 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
2123
2123
  expect(src).to include('byteslice(0, 2_048)')
2124
2124
  end
2125
2125
 
2126
- describe 'thinking on the task TUI line' do
2127
- it 'prefers model thinking over the TaskSummarizer brief' do
2126
+ describe 'thinking on the session pane' do
2127
+ it 'keeps the task brief and emits thinking separately' do
2128
2128
  allow(PWN::AI::Agent::TaskSummarizer).to receive(:enabled?).and_return(true)
2129
2129
  allow(PWN::AI::Agent::TaskSummarizer).to receive(:about_to).and_return('task 1/3: scan')
2130
2130
  seen = []
2131
2131
  described_class.send(
2132
- :task_summary_about_to!,
2133
- state: { plan: ['scan'] },
2134
- tools: [{ name: 'shell' }],
2135
- request: 'scan the host',
2132
+ :emit_thinking!,
2136
2133
  thinking: 'Need to fingerprint the service before scanning.',
2137
2134
  on_tool: ->(name, args, _res) { seen << [name, args] }
2138
2135
  )
2139
- expect(seen).to eq([['task', 'Need to fingerprint the service before scanning.']])
2140
- expect(PWN::AI::Agent::TaskSummarizer).not_to have_received(:about_to)
2141
- end
2142
-
2143
- it 'falls back to the TaskSummarizer brief when thinking is blank' do
2144
- allow(PWN::AI::Agent::TaskSummarizer).to receive(:enabled?).and_return(true)
2145
- allow(PWN::AI::Agent::TaskSummarizer).to receive(:about_to).and_return('task 1/3: scan')
2146
- seen = []
2147
2136
  described_class.send(
2148
2137
  :task_summary_about_to!,
2149
2138
  state: { plan: ['scan'] },
2150
2139
  tools: [{ name: 'shell' }],
2151
2140
  request: 'scan the host',
2152
- thinking: ' ',
2153
2141
  on_tool: ->(name, args, _res) { seen << [name, args] }
2154
2142
  )
2155
- expect(seen).to eq([['task', 'task 1/3: scan']])
2143
+ expect(seen).to eq([
2144
+ ['thinking', 'Need to fingerprint the service before scanning.'],
2145
+ ['task', 'task 1/3: scan']
2146
+ ])
2147
+ end
2148
+
2149
+ it 'does not emit a blank thinking row' do
2150
+ seen = []
2151
+ described_class.send(:emit_thinking!, thinking: ' ', on_tool: ->(name, args, _res) { seen << [name, args] })
2152
+ expect(seen).to eq([])
2156
2153
  end
2157
2154
  end
2158
2155
 
@@ -389,6 +389,19 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
389
389
  expect(colors).to eq(operator_label: 4, operator_body: 5, assistant: 5, task: 2, tool: 1, result: 3)
390
390
  end
391
391
 
392
+ it 'shows model thinking in the session pane even when compact' do
393
+ pry = Pry.new
394
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: pry, input: StringIO.new, curses: nil, getch: nil)
395
+ console.instance_variable_set(:@verbose, false)
396
+ console.add(:thinking, 'The lemon is yellow because of carotenoids.')
397
+ console.add(:assistant, 'Yellow.')
398
+ text = console.timeline_rows(60).map { |_color, row| row }.join("\n")
399
+ expect(text).to include('THINKING')
400
+ expect(text).to include('The lemon is yellow because of carotenoids.')
401
+ expect(text).to include('ASSISTANT')
402
+ expect(text).to include('Yellow.')
403
+ end
404
+
392
405
  it 'uses template colors for every unconfigured role without mutating defaults or overrides' do
393
406
  defaults = PWN::Config.env_template.dig(:ai, :tui, :theme)
394
407
  [nil, {}, 'invalid'].each do |configured|
@@ -403,6 +403,7 @@
403
403
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.dispatch_fail_n Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.dispatch_fail_n`: "}]}
404
404
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.duration_unsatisfied? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.duration_unsatisfied?`: "}]}
405
405
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.emit_task_summary Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.emit_task_summary`: "}]}
406
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.emit_thinking! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.emit_thinking!`: "}]}
406
407
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.engine_transient? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.engine_transient?`: "}]}
407
408
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.escalate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.escalate`: "}]}
408
409
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_blob Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_blob`: "}]}
@@ -1206,6 +1207,8 @@
1206
1207
  {"messages":[{"role":"user","content":"PWN::AI::Ollama.ollama_rest_call Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.ollama_rest_call`: Supported Method Parameters\n\nollama_rest_call(\n\nhttp_method: 'optional HTTP method (defaults to GET)',\nrest_call: 'required rest call path relative to base_uri (e.g. api/chat)',\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"}]}
1207
1208
  {"messages":[{"role":"user","content":"PWN::AI::Ollama.parse_tool_arguments_object Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.parse_tool_arguments_object`: "}]}
1208
1209
  {"messages":[{"role":"user","content":"PWN::AI::Ollama.real_config_value? Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.real_config_value?`: "}]}
1210
+ {"messages":[{"role":"user","content":"PWN::AI::Ollama.strict_system_template? Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.strict_system_template?`: "}]}
1211
+ {"messages":[{"role":"user","content":"PWN::AI::Ollama.template_rejects_later_system? Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.template_rejects_later_system?`: "}]}
1209
1212
  {"messages":[{"role":"user","content":"PWN::AI::Ollama.visible_from_thinking Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.visible_from_thinking`: "}]}
1210
1213
  {"messages":[{"role":"user","content":"PWN::AI::OpenAI.api_endpoint Usage"},{"role":"assistant","content":"`PWN::AI::OpenAI.api_endpoint`: "}]}
1211
1214
  {"messages":[{"role":"user","content":"PWN::AI::OpenAI.authors Usage"},{"role":"assistant","content":"`PWN::AI::OpenAI.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
@@ -1258,6 +1261,8 @@
1258
1261
  {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.openwebui_rest_call Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.openwebui_rest_call`: Supported Method Parameters\n\nopenwebui_rest_call(\n\nhttp_method: 'optional HTTP method (defaults to GET)',\nrest_call: 'required rest call path relative to base_uri',\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"}]}
1259
1262
  {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.parse_tool_arguments_object Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.parse_tool_arguments_object`: "}]}
1260
1263
  {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.real_config_value? Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.real_config_value?`: "}]}
1264
+ {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.strict_system_template? Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.strict_system_template?`: "}]}
1265
+ {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.template_rejects_later_system? Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.template_rejects_later_system?`: "}]}
1261
1266
  {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.visible_from_thinking Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.visible_from_thinking`: "}]}
1262
1267
  {"messages":[{"role":"user","content":"PWN::AI::RedTeam.authors Usage"},{"role":"assistant","content":"`PWN::AI::RedTeam.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
1263
1268
  {"messages":[{"role":"user","content":"PWN::AI::RedTeam.help Usage"},{"role":"assistant","content":"`PWN::AI::RedTeam.help`: "}]}
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.758
4
+ version: 0.5.759
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -771,14 +771,14 @@ dependencies:
771
771
  requirements:
772
772
  - - '='
773
773
  - !ruby/object:Gem::Version
774
- version: 1.6.3
774
+ version: 1.7.0
775
775
  type: :development
776
776
  prerelease: false
777
777
  version_requirements: !ruby/object:Gem::Requirement
778
778
  requirements:
779
779
  - - '='
780
780
  - !ruby/object:Gem::Version
781
- version: 1.6.3
781
+ version: 1.7.0
782
782
  - !ruby/object:Gem::Dependency
783
783
  name: pry
784
784
  requirement: !ruby/object:Gem::Requirement