pwn 0.5.720 → 0.5.721

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/etc/default_skills/pwn/ai/agent/loop/SKILL.md +4 -2
  3. data/etc/default_skills/pwn/ai/agent/metrics/SKILL.md +1 -0
  4. data/etc/default_skills/pwn/ai/agent/registry/SKILL.md +3 -0
  5. data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +2 -0
  6. data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +2 -0
  7. data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +1 -0
  8. data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
  9. data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +1 -0
  10. data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
  11. data/lib/pwn/ai/agent/dispatch.rb +6 -5
  12. data/lib/pwn/ai/agent/learning.rb +17 -1
  13. data/lib/pwn/ai/agent/loop.rb +63 -25
  14. data/lib/pwn/ai/agent/metrics.rb +35 -1
  15. data/lib/pwn/ai/agent/registry.rb +28 -0
  16. data/lib/pwn/ai/agent/reward.rb +4 -3
  17. data/lib/pwn/ai/agent/tool_guard.rb +54 -9
  18. data/lib/pwn/ai/agent/turn_finalizer.rb +20 -1
  19. data/lib/pwn/plugins/artifact_registry.rb +32 -2
  20. data/lib/pwn/plugins/exploit_dev.rb +33 -0
  21. data/lib/pwn/plugins/findings.rb +9 -1
  22. data/lib/pwn/plugins/ghidra.rb +20 -2
  23. data/lib/pwn/plugins/preflight_checker.rb +23 -0
  24. data/lib/pwn/plugins/process_tube.rb +31 -1
  25. data/lib/pwn/sessions.rb +4 -0
  26. data/lib/pwn/version.rb +1 -1
  27. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +9 -0
  28. data/spec/lib/pwn/ai/agent/loop_spec.rb +44 -0
  29. data/spec/lib/pwn/ai/agent/reward_spec.rb +1 -1
  30. data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +9 -0
  31. data/spec/lib/pwn/plugins/findings_spec.rb +4 -4
  32. data/third_party/pwn_rdoc.jsonl +11 -0
  33. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0dd0d654d48c9ead7f9a92893f88443e1cfd7cd9a867c53cb2ffc25cbb60fe27
4
- data.tar.gz: 3960e8501cbd0139184cd0981569a28a4f9b37e47ddddeb5dab001b2d0eefb1b
3
+ metadata.gz: c26adc03f68a0e0275c0eb1a85aa2bb06def95aeb014411b000f52c6a83e3e40
4
+ data.tar.gz: 6a082b81f8f1885de1b3ac42c604f1f21a08d48e7260609ce35e00d1461afa79
5
5
  SHA512:
6
- metadata.gz: defca7be52f6960bbaa318f5e40e4927cafa8a1e679ce27dd8e74b73feae4b89a9766b725a5731b2e2fa5a8e93c58a2fa3622f0c37df6b0355bf6b12e9affbae
7
- data.tar.gz: 29de8ee9497ec7004c4752ddaf3fc359016c6007368e051e891e191ad18bf069a3a44d32aa73ed480dc60cb30bba46551efa2f636009b8d50752e0101917900b
6
+ metadata.gz: 719d713266b6a3d4200895bb41d4ac5ec7ccfc6d142463cb70085cd6c8d02a514191553b5b12317de45a38abf3d4d1ccb47101f54dc5e588e596a13bc3e77af8
7
+ data.tar.gz: 271d9ff938ac37275ebe9da7d3031305bd09f587e5b6d14f0386fc006bd120b6e684b00aa651c935da7eefbd3a71c298b114ef07ec21b7a9099a75d9f71df647
@@ -28,11 +28,12 @@ Class methods take `(opts = {})` and read `opts`.
28
28
 
29
29
  ```ruby
30
30
  PWN::AI::Agent::Loop.help
31
- PWN::AI::Agent::Loop.debug_on(opts)
31
+ PWN::AI::Agent::Loop.evidence_satisfied(opts)
32
32
  ```
33
33
 
34
34
  ## Public methods
35
35
 
36
+ - `evidence_satisfied`
36
37
  - `debug_on`
37
38
  - `catalog_lookup`
38
39
  - `world_knowledge`
@@ -46,6 +47,7 @@ PWN::AI::Agent::Loop.debug_on(opts)
46
47
  - `help`
47
48
  - `catalog_lookup?`
48
49
  - `debug_on?`
50
+ - `evidence_satisfied?`
49
51
  - `needs_host_work?`
50
52
  - `world_knowledge?`
51
53
 
@@ -59,5 +61,5 @@ PWN::AI::Agent::Loop.debug_on(opts)
59
61
 
60
62
  ## Verification
61
63
 
62
- `PWN::AI::Agent::Loop.respond_to?(:debug_on)` after the
64
+ `PWN::AI::Agent::Loop.respond_to?(:evidence_satisfied)` after the
63
65
  module is loaded. Read the source for parameter names.
@@ -36,6 +36,7 @@ PWN::AI::Agent::Metrics.load(opts)
36
36
  - `load`
37
37
  - `save`
38
38
  - `record`
39
+ - `append_jsonl`
39
40
  - `summary`
40
41
  - `to_context`
41
42
  - `proxy_trust`
@@ -42,8 +42,11 @@ PWN::AI::Agent::Registry.register(opts)
42
42
  - `apply_preference`
43
43
  - `rank`
44
44
  - `discover`
45
+ - `eager_load`
46
+ - `selftest`
45
47
  - `authors`
46
48
  - `help`
49
+ - `eager_load!`
47
50
 
48
51
  ## Source
49
52
 
@@ -61,6 +61,7 @@ PWN::AI::Agent::ToolGuard.present(opts)
61
61
  - `refuse_copied_persist`
62
62
  - `scope_refusal`
63
63
  - `policy_decision`
64
+ - `scope_check`
64
65
  - `rfc1918`
65
66
  - `ip_in_cidr`
66
67
  - `command_class`
@@ -81,6 +82,7 @@ PWN::AI::Agent::ToolGuard.present(opts)
81
82
  - `refuse_copied_persist?`
82
83
  - `reset_timeout_budget!`
83
84
  - `rfc1918?`
85
+ - `scope_check!`
84
86
  - `shell_bash?`
85
87
 
86
88
  ## Source
@@ -43,9 +43,11 @@ PWN::AI::Agent::TurnFinalizer.enter_user_path(opts)
43
43
  - `pending`
44
44
  - `arbitrate`
45
45
  - `evidence_ledger`
46
+ - `evidence_satisfied`
46
47
  - `authors`
47
48
  - `help`
48
49
  - `enter_user_path!`
50
+ - `evidence_satisfied?`
49
51
  - `join_all!`
50
52
  - `leave_user_path!`
51
53
  - `should_defer?`
@@ -38,6 +38,7 @@ PWN::Plugins::ArtifactRegistry.required_bins(opts)
38
38
  - `list`
39
39
  - `get`
40
40
  - `read_page`
41
+ - `put`
41
42
  - `authors`
42
43
  - `help`
43
44
 
@@ -52,6 +52,7 @@ PWN::Plugins::ExploitDev.required_bins(opts)
52
52
  - `gadgets`
53
53
  - `fmtstr`
54
54
  - `io`
55
+ - `scaffold`
55
56
  - `authors`
56
57
  - `help`
57
58
 
@@ -37,6 +37,7 @@ PWN::Plugins::PreflightChecker.required_bins(opts)
37
37
  - `bin`
38
38
  - `require_bin`
39
39
  - `route`
40
+ - `capability_coverage`
40
41
  - `pick`
41
42
  - `cap_net_raw`
42
43
  - `require_cap_net_raw`
@@ -43,6 +43,8 @@ PWN::Plugins::ProcessTube.required_bins(opts)
43
43
  - `expect`
44
44
  - `stream`
45
45
  - `reap_orphans`
46
+ - `list`
47
+ - `kill`
46
48
  - `authors`
47
49
  - `help`
48
50
 
@@ -52,7 +52,8 @@ module PWN
52
52
  args = expand_vault_args(args: args)
53
53
  end
54
54
  if defined?(Engagement)
55
- denied = Engagement.deny_if_out_of_scope(args: args, command: blob)
55
+ denied = ToolGuard.scope_check!(args: args, command: blob) if defined?(ToolGuard) && ToolGuard.respond_to?(:scope_check!)
56
+ denied ||= Engagement.deny_if_out_of_scope(args: args, command: blob)
56
57
  return JSON.generate(denied) if denied
57
58
  end
58
59
  if defined?(ToolGuard) && ToolGuard.respond_to?(:policy_decision)
@@ -343,8 +344,8 @@ module PWN
343
344
  return :store if STORE_TOOLS.include?(name)
344
345
 
345
346
  blob = argv_blob(args: opts[:args])
346
- return :browse if blob.match?(BROWSE_ARGV_RX)
347
347
  return :write if blob.match?(WRITE_ARGV_RX)
348
+ return :browse if blob.match?(BROWSE_ARGV_RX)
348
349
  return :eval if name == 'pwn_eval'
349
350
 
350
351
  :read
@@ -381,7 +382,7 @@ module PWN
381
382
 
382
383
  private_class_method def self.note_taint(opts = {})
383
384
  text = opts[:text].to_s
384
- grams = text.scan(/.{12,}/).first(20)
385
+ grams = text.scan(/.{24,}/).first(20)
385
386
  store = Thread.current[:pwn_taint] ||= []
386
387
  grams.each { |g| store << g[0, 64] }
387
388
  store.shift while store.length > 200
@@ -393,10 +394,10 @@ module PWN
393
394
  return false if mode == 'off'
394
395
 
395
396
  blob = opts[:args].inspect
396
- return false if blob.length < 12
397
+ return false if blob.length < 24
397
398
  return false if opts[:args].is_a?(Hash) && (opts[:args][:taint_ack] == true || opts[:args]['taint_ack'] == true)
398
399
 
399
- hit = Array(Thread.current[:pwn_taint]).any? { |g| g.length >= 12 && blob.include?(g) }
400
+ hit = Array(Thread.current[:pwn_taint]).any? { |g| g.length >= 24 && blob.include?(g) }
400
401
  return false unless hit
401
402
  return false unless blob.match?(/curl |bash -c|sh -c|\|\s*sh\b/i)
402
403
 
@@ -196,11 +196,21 @@ module PWN
196
196
  jmean = total.positive? ? weighted_judge_mean(rows: rows) : nil
197
197
  distrust = 0.0
198
198
  distrust = Reward.proxy_distrust.to_f.clamp(0.0, 1.0) if defined?(Reward) && Reward.respond_to?(:proxy_distrust)
199
+ orm = rows.select { |r| r[:judge_source].to_s != 'heuristic' && r[:source].to_s != 'heuristic' }
200
+ heur = rows.select { |r| r[:judge_source].to_s == 'heuristic' || r[:source].to_s == 'heuristic' }
201
+ orm_n = orm.length
202
+ heur_n = heur.length
203
+ orm_ok = orm.count { |r| r[:success] == true }
204
+ heur_ok = heur.count { |r| r[:success] == true }
199
205
  {
200
206
  total_outcomes: total,
201
207
  successes: ok,
202
208
  failures: total - ok,
203
209
  success_rate: raw,
210
+ success_rate_orm: orm_n.positive? ? (orm_ok.to_f / orm_n).round(3) : 0.0,
211
+ success_rate_heur: heur_n.positive? ? (heur_ok.to_f / heur_n).round(3) : 0.0,
212
+ orm_n: orm_n,
213
+ heur_n: heur_n,
204
214
  adjusted_success_rate: discount_success_rate(proxy: raw, judge: jmean, distrust: distrust),
205
215
  proxy_distrust: distrust,
206
216
  skills_known: skills,
@@ -227,6 +237,12 @@ module PWN
227
237
  fails = prefer_primary_tasks(rows: outcomes(limit: 200, success: false))
228
238
  fails = fails.reject { |r| r[:status].to_s == 'conflicted' }
229
239
  fails = fails.reject { |r| r[:verifier_verdict].to_s == 'pass' }
240
+ fails = fails.reject { |r| r[:details].to_s.match?(/\bPASS\b/) && r[:success] == true }
241
+ fails = fails.select do |r|
242
+ v = r[:verdict].to_s
243
+ v == 'wrong' || v == 'refused' || (r[:score].to_f < 0.3 && !r[:details].to_s.match?(/\bPASS\b/)) || r[:success] == false
244
+ end
245
+ fails = fails.reject { |r| r[:success] == true }
230
246
  # Do not mirror the same ids under both headings — that doubled the
231
247
  # failure signal and made RECENT OUTCOMES == RECENT FAILURES when the
232
248
  # last N attempts all failed (the injected block looked "stuck").
@@ -260,7 +276,7 @@ module PWN
260
276
  d = s[:proxy_distrust].to_f
261
277
  rate = d > 0.05 ? s[:adjusted_success_rate] : s[:success_rate]
262
278
  tag = d > 0.05 ? ' adj' : ''
263
- hdr = "RECENT OUTCOMES (success_rate=#{(rate.to_f * 100).round(1)}%#{tag}#{" judge_mean=#{jm}" if jm} over #{s[:total_outcomes]} attempts)"
279
+ hdr = "RECENT OUTCOMES (success_rate=#{(rate.to_f * 100).round(1)}%#{tag} success=orm:#{(s[:success_rate_orm].to_f * 100).round(1)}%(#{s[:orm_n]}) / heur:#{(s[:success_rate_heur].to_f * 100).round(1)}%(#{s[:heur_n]})#{" judge_mean=#{jm}" if jm} over #{s[:total_outcomes]} attempts)"
264
280
  out = "#{hdr}\n#{rows.map(&fmt).join("\n")}\n"
265
281
  out += "RECENT FAILURES (learn from these — do not repeat)\n#{fails.map(&fmt).join("\n")}\n" unless fails.empty?
266
282
  "#{out}\n"
@@ -64,6 +64,18 @@ module PWN
64
64
  unsatisfied incomplete_final empty_final evidence_final
65
65
  ].freeze
66
66
 
67
+ public_class_method def self.evidence_satisfied?(opts = {})
68
+ messages = Array(opts[:messages] || opts[:trace])
69
+ text = opts[:text].to_s
70
+ if defined?(TurnFinalizer) && TurnFinalizer.respond_to?(:arbitrate)
71
+ row = TurnFinalizer.arbitrate(request: opts[:request].to_s, messages: messages, paths: [])
72
+ return true if row[:complete] && row[:unmet].empty? && row[:ledger].any? { |_p, v| v[:write] && v[:read] }
73
+ end
74
+ write_or_read_evidenced?(messages: messages) && !text.strip.empty?
75
+ rescue StandardError
76
+ false
77
+ end
78
+
67
79
  public_class_method def self.debug_on?(opts = {})
68
80
  return true if opts[:debug]
69
81
  return true if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.debug_enabled?
@@ -394,6 +406,7 @@ module PWN
394
406
  # the original request → force synthesis. English tasks are an advisory
395
407
  # compass only — an open verify tail must not block a finished ask.
396
408
  # The original request is the completion signal.
409
+
397
410
  private_class_method def self.evidence_enough_to_finalize?(opts = {})
398
411
  messages = Array(opts[:messages])
399
412
  turn_fails = opts[:turn_fails] || {}
@@ -595,7 +608,7 @@ module PWN
595
608
  request: request,
596
609
  messages: opts[:messages]
597
610
  )
598
- return true if need == :write && !write_verified?(effects: effects)
611
+ return true if need == :write && !write_verified?(effects: effects, request: request, messages: opts[:messages])
599
612
  return true if need == :browse && !effects.include?(:browse)
600
613
  return true if need == :any && !effects.intersect?(%i[write browse eval])
601
614
 
@@ -607,10 +620,27 @@ module PWN
607
620
  private_class_method def self.write_verified?(opts = {})
608
621
  effects = Array(opts[:effects])
609
622
  idx = effects.index(:write)
610
- return false if idx.nil?
623
+ if idx
624
+ tail = effects[(idx + 1)..] || []
625
+ return true if tail.intersect?(%i[read eval])
626
+ end
627
+
628
+ saw_write_argv = false
629
+ Array(opts[:messages]).each do |msg|
630
+ next unless msg.is_a?(Hash)
631
+
632
+ if msg[:role].to_s == 'assistant' && defined?(Dispatch)
633
+ Array(msg[:tool_calls]).each do |tc|
634
+ blob = Dispatch.send(:argv_blob, args: tc.dig(:function, :arguments) || tc.dig('function', 'arguments'))
635
+ saw_write_argv = true if blob.to_s.match?(Dispatch::WRITE_ARGV_RX)
636
+ end
637
+ elsif saw_write_argv && msg[:role].to_s == 'tool'
638
+ fx = stamped_effect(content: msg[:content])
639
+ return true if %i[read eval].include?(fx)
640
+ end
641
+ end
611
642
 
612
- tail = effects[(idx + 1)..] || []
613
- tail.intersect?(%i[read eval])
643
+ false
614
644
  rescue StandardError
615
645
  false
616
646
  end
@@ -875,6 +905,7 @@ module PWN
875
905
  end
876
906
 
877
907
  private_class_method def self.may_finalize?(opts = {})
908
+ return true if evidence_satisfied?(messages: opts[:messages], text: opts[:text], request: opts[:request])
878
909
  return false if incomplete_final?(text: opts[:text], last_iter: false)
879
910
  return false if request_unsatisfied?(
880
911
  request: opts[:request],
@@ -1678,6 +1709,7 @@ module PWN
1678
1709
  }
1679
1710
  # Ollama + abliterated / weak chat-templates often ignore tools: and
1680
1711
  # answer in prose (or print shell(...) as text). Force native
1712
+
1681
1713
  # tool_calls until at least one tool result is already in history;
1682
1714
  # after that, auto so the model can emit a real final answer.
1683
1715
  # Respect explicit PWN::Env[:ai][:ollama][:tool_choice] override.
@@ -1705,10 +1737,6 @@ module PWN
1705
1737
  end
1706
1738
  end
1707
1739
 
1708
- # 3.1 — sliding-window history compaction for local models.
1709
- # Keep: system, original user, PLAN assistant (if any), last K tool
1710
- # pairs (assistant+tool), and the most recent assistant. Stale tool
1711
- # bodies are truncated to history_tool_max_chars.
1712
1740
  private_class_method def self.session_chat_history(opts = {})
1713
1741
  return [] unless defined?(PWN::Sessions) && PWN::Sessions.respond_to?(:to_llm_messages)
1714
1742
 
@@ -1831,15 +1859,15 @@ module PWN
1831
1859
 
1832
1860
  private_class_method def self.spill_tool_body(opts = {})
1833
1861
  text = opts[:text].to_s
1834
- digest = Digest::SHA256.hexdigest(text)[0, 16]
1835
- dir = HISTORY_SPILL_DIR
1862
+ digest = Digest::SHA256.hexdigest(text)
1863
+ dir = File.join(Dir.home, '.pwn', 'artifacts', 'transcripts', (Thread.current[:pwn_session_id] || 'default').to_s)
1836
1864
  FileUtils.mkdir_p(dir)
1837
- path = File.join(dir, "#{digest}.txt")
1865
+ path = File.join(dir, "#{digest[0, 12]}.txt")
1838
1866
  File.binwrite(path, text) unless File.file?(path)
1839
1867
  head = text.byteslice(0, 2_048).to_s
1840
1868
  tail = text.bytesize > 3_072 ? text.byteslice(-1_024, 1_024).to_s : ''
1841
1869
  mid = tail.empty? ? '' : "\n...\n#{tail}"
1842
- "#{head}#{mid}\n[compacted path=#{path} sha256=#{digest} bytes=#{text.bytesize}]"
1870
+ "#{head}#{mid}\n[compacted path=#{path} sha256=#{digest} bytes=#{text.bytesize} ref=#{path}]"
1843
1871
  rescue StandardError
1844
1872
  "[compacted bytes=#{opts[:text].to_s.bytesize}]"
1845
1873
  end
@@ -3002,17 +3030,21 @@ module PWN
3002
3030
  text: text
3003
3031
  )
3004
3032
  turn_fails['unsatisfied'] += 1
3005
- unmet = completion_unmet(request: request, messages: messages)
3006
- warn "[pwn-ai/loop] original request not evidenced on iter=#{i} unmet=#{unmet.join(',')}; continuing"
3007
- debug_progress(msg: "bounce unsatisfied unmet=#{unmet.join(',')} snippet=#{debug_snippet(text: text)}")
3008
- messages << {
3009
- role: 'user',
3010
- content: "[pwn-ai] The original request is not evidenced yet. unmet=#{unmet.join(',')} " \
3011
- 'Keep calling CORE_TOOLS (shell, pwn_eval) until that request is ' \
3012
- 'done or a tool returned failure evidence. pwn-ai does not decide ' \
3013
- 'authorization. Do not declare completion from a listing or a refusal.'
3014
- }
3015
- next
3033
+ if turn_fails['unsatisfied'] >= 2 && evidence_satisfied?(request: request, messages: messages, text: text)
3034
+ debug_progress(msg: 'nag cap: evidence_satisfied after 2 bounces')
3035
+ else
3036
+ unmet = completion_unmet(request: request, messages: messages)
3037
+ warn "[pwn-ai/loop] original request not evidenced on iter=#{i} unmet=#{unmet.join(',')}; continuing"
3038
+ debug_progress(msg: "bounce unsatisfied unmet=#{unmet.join(',')} snippet=#{debug_snippet(text: text)}")
3039
+ messages << {
3040
+ role: 'user',
3041
+ content: "[pwn-ai] The original request is not evidenced yet. unmet=#{unmet.join(',')} " \
3042
+ 'Keep calling CORE_TOOLS (shell, pwn_eval) until that request is ' \
3043
+ 'done or a tool returned failure evidence. pwn-ai does not decide ' \
3044
+ 'authorization. Do not declare completion from a listing or a refusal.'
3045
+ }
3046
+ next
3047
+ end
3016
3048
  end
3017
3049
  debug_progress(msg: "final accepted chars=#{text.to_s.length}")
3018
3050
  quiet_debug_tui!(reason: 'final')
@@ -3163,8 +3195,6 @@ module PWN
3163
3195
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
3164
3196
  end
3165
3197
 
3166
- # Display Usage for this Module
3167
-
3168
3198
  public_class_method def self.help
3169
3199
  puts "USAGE:
3170
3200
  # Run debug on and return its result
@@ -3172,6 +3202,14 @@ module PWN
3172
3202
  debug: 'optional - debug value consumed by #debug_on?'
3173
3203
  )
3174
3204
 
3205
+ # True when write-then-readback evidence satisfies the original request.
3206
+ #{self}.evidence_satisfied?(
3207
+ messages: 'optional - Array of role/content hashes',
3208
+ trace: 'optional - alias for messages',
3209
+ text: 'optional - final answer text',
3210
+ request: 'optional - original request'
3211
+ )
3212
+
3175
3213
  # True only when the ask needs a live host/file/browser effect. World-knowledge
3176
3214
  #{self}.catalog_lookup?(
3177
3215
  request: 'required - request value consumed by #catalog_lookup?'
@@ -99,9 +99,32 @@ module PWN
99
99
  bump(bucket: e, success: success, duration: duration, error: error)
100
100
  end
101
101
  save(metrics: metrics)
102
+ append_jsonl(opts.merge(name: name, success: success, duration: duration, error: error, engine: engine))
102
103
  t
103
104
  end
104
105
 
106
+ public_class_method def self.append_jsonl(opts = {})
107
+ path = File.join(Dir.home, '.pwn', 'logs', 'tool_metrics.jsonl')
108
+ FileUtils.mkdir_p(File.dirname(path))
109
+ row = {
110
+ ts: Time.now.utc.iso8601,
111
+ session: Thread.current[:pwn_session_id],
112
+ tool: opts[:name],
113
+ latency_ms: (opts[:duration].to_f * 1000).round,
114
+ timeout_used: opts[:timeout],
115
+ outcome: opts[:success] ? 'ok' : 'err',
116
+ error_class: opts[:error].to_s.split(':').first,
117
+ bytes_out: opts[:bytes_out].to_i
118
+ }
119
+ File.open(path, 'a') do |f|
120
+ f.flock(File::LOCK_EX)
121
+ f.puts(JSON.generate(row))
122
+ end
123
+ row
124
+ rescue StandardError
125
+ nil
126
+ end
127
+
105
128
  # Supported Method Parameters::
106
129
  # rows = PWN::AI::Agent::Metrics.summary(
107
130
  # limit: 'optional - cap number of tools returned (default 25)',
@@ -664,11 +687,22 @@ module PWN
664
687
  #{self}.record(
665
688
  name: 'required - tool name that was dispatched',
666
689
  success: 'required - Boolean, did the handler complete without error',
667
- duration: 'optional - Float seconds the dispatch took',
690
+ duration: 'optional - Float seconds the dispatch took (wall time)',
668
691
  error: 'optional - String error message when success is false',
669
692
  engine: 'optional - Symbol/String AI engine that chose this tool (segments telemetry)'
670
693
  )
671
694
 
695
+ # Append one JSONL telemetry row under ~/.pwn/logs/tool_metrics.jsonl.
696
+ #{self}.append_jsonl(
697
+ name: 'required - tool name',
698
+ success: 'optional - Boolean outcome',
699
+ duration: 'optional - Float seconds the dispatch took (wall time)',
700
+ error: 'optional - error string',
701
+ engine: 'optional - engine name',
702
+ timeout: 'optional - timeout used',
703
+ bytes_out: 'optional - output byte count'
704
+ )
705
+
672
706
  # Run summary and return its result
673
707
  #{self}.summary(
674
708
  limit: 'optional - cap number of tools returned (default 25)',
@@ -113,6 +113,7 @@ module PWN
113
113
  # )
114
114
 
115
115
  public_class_method def self.definitions(opts = {})
116
+ eager_load!
116
117
  enabled = opts[:enabled]
117
118
  enabled = enabled.map(&:to_s) if enabled
118
119
  pool = @entries.values.select { |e| (enabled.nil? || enabled.include?(e.toolset)) && safe_check(entry: e) }
@@ -296,6 +297,10 @@ module PWN
296
297
  # )
297
298
 
298
299
  public_class_method def self.discover(opts = {})
300
+ eager_load!(opts)
301
+ end
302
+
303
+ public_class_method def self.eager_load!(opts = {})
299
304
  force = opts[:force] ||= false
300
305
  return @entries.keys if @discovered && !force
301
306
 
@@ -311,6 +316,19 @@ module PWN
311
316
  @entries.keys
312
317
  end
313
318
 
319
+ public_class_method def self.selftest(opts = {})
320
+ _dry = opts[:dry_run]
321
+ eager_load!
322
+ tools_dir = File.join(__dir__, 'tools')
323
+ files = Dir[File.join(tools_dir, '*.rb')]
324
+ rows = @entries.map do |name, e|
325
+ schema = e.schema.is_a?(Hash)
326
+ required = Array(e.schema&.dig(:parameters, :required) || e.schema&.dig('parameters', 'required'))
327
+ { name: name, schema_valid: schema, handler: !e.handler.nil?, required: required }
328
+ end
329
+ { tools: rows, files: files.length, registered: @entries.length, ok: rows.length >= files.length }
330
+ end
331
+
314
332
  private_class_method def self.safe_check(opts = {})
315
333
  entry = opts[:entry]
316
334
  entry.check.call
@@ -428,6 +446,16 @@ module PWN
428
446
  force: 'optional - re-require tool files even if already discovered (default false)'
429
447
  )
430
448
 
449
+ # Eager-load every tools/*.rb so Registry.definitions works in a cold REPL.
450
+ #{self}.eager_load!(
451
+ force: 'optional - re-require tool files even if already discovered (default false)'
452
+ )
453
+
454
+ # Schema/handler contract check for every registered tool.
455
+ #{self}.selftest(
456
+ dry_run: 'optional - reserved dry-run flag'
457
+ )
458
+
431
459
  # Print the AUTHOR(S) string for this module.
432
460
  #{self}.authors
433
461
  "
@@ -203,10 +203,11 @@ module PWN
203
203
  v[:engine] = eng
204
204
  v[:task_class] = request.match?(/analy[sz]e|summar|strength|weakness|fitness/i) ? 'analysis' : 'operational'
205
205
  if pass
206
- v[:score] = [v[:score].to_f, 0.7].max
207
- v[:score] = [v[:score].to_f, 0.70].min
206
+ score = v[:score].to_f
207
+ v[:score] = [score, 0.6].max
208
208
  v[:success] = true if prec
209
209
  v[:verifier_verdict] ||= :pass
210
+ v[:verdict] = :solved
210
211
  end
211
212
  v[:score_components] ||= {
212
213
  judge: v[:score].to_f,
@@ -1099,7 +1100,7 @@ module PWN
1099
1100
  CHEAP_ORM_TEMP = 0.1
1100
1101
  CHEAP_ORM_TRACE_N = 12
1101
1102
  ORM_SAMPLE_WEIGHT = 1.0
1102
- HEURISTIC_SAMPLE_WEIGHT = 0.25
1103
+ HEURISTIC_SAMPLE_WEIGHT = 0.45
1103
1104
  ERROR_SAMPLE_WEIGHT = 0.15
1104
1105
  ENGINE_CHAT_MODS = {
1105
1106
  openai: 'PWN::AI::OpenAI',
@@ -5,6 +5,9 @@ require 'json'
5
5
  require 'fileutils'
6
6
  require 'securerandom'
7
7
  require 'time'
8
+ require 'openssl'
9
+ require 'uri'
10
+ require 'base64'
8
11
 
9
12
  module PWN
10
13
  module AI
@@ -41,7 +44,12 @@ module PWN
41
44
  end
42
45
 
43
46
  public_class_method def self.placeholder?(opts = {})
44
- PLACEHOLDER_RX.match?(opts[:text].to_s)
47
+ s = opts[:text].to_s.dup
48
+ s.gsub!(/<<[-~]?\s*(['"])(\w+)\1.*?^\2\s*$/m, ' ')
49
+ s.gsub!(/<<[-~]?\s*(\w+).*?^\1\s*$/m, ' ')
50
+ s.gsub!(/'[^']*'/, "''")
51
+ s.gsub!(/"([^"\\]|\\.)*"/, '""')
52
+ PLACEHOLDER_RX.match?(s)
45
53
  rescue StandardError
46
54
  false
47
55
  end
@@ -60,18 +68,36 @@ module PWN
60
68
  end
61
69
 
62
70
  public_class_method def self.mint_canary(opts = {})
63
- n = (opts[:bytes] || 8).to_i
64
- n = 8 if n <= 0
65
- tok = "PWNCANARY#{SecureRandom.hex(n)}"
71
+ _bytes = opts[:bytes]
72
+ turn = (opts[:turn] || Thread.current[:pwn_loop_iter] || 0).to_i
73
+ sid = (opts[:session_id] || Thread.current[:pwn_session_id] || 'sess').to_s
74
+ key = (Thread.current[:pwn_canary_key] ||= SecureRandom.hex(16))
75
+ tok = OpenSSL::HMAC.hexdigest('SHA256', key, "#{sid}:#{turn}")[0, 16]
76
+ hist = Thread.current[:pwn_canary_hist] ||= []
77
+ hist << tok
78
+ hist.shift while hist.length > 4
66
79
  Thread.current[:pwn_canary] = tok
67
80
  tok
68
81
  end
69
82
 
70
83
  public_class_method def self.canary_leak?(opts = {})
71
- tok = Thread.current[:pwn_canary].to_s
72
- return false if tok.empty?
73
-
74
- opts[:text].to_s.include?(tok)
84
+ text = opts[:text].to_s
85
+ toks = Array(Thread.current[:pwn_canary_hist])
86
+ toks << Thread.current[:pwn_canary].to_s
87
+ toks = toks.reject(&:empty?).uniq
88
+ return false if toks.empty?
89
+
90
+ require 'base64'
91
+ toks.any? do |tok|
92
+ next true if text.include?(tok)
93
+
94
+ b64 = Base64.strict_encode64(tok)
95
+ b64u = Base64.urlsafe_encode64(tok)
96
+ hex = tok.each_byte.map { |b| format('%02x', b) }.join
97
+ enc = URI.encode_www_form_component(tok)
98
+ rot = tok.tr('A-Za-z', 'N-ZA-Mn-za-m')
99
+ text.include?(b64) || text.include?(b64u) || text.include?(hex) || text.include?(enc) || text.include?(rot)
100
+ end
75
101
  end
76
102
 
77
103
  public_class_method def self.injection_score(opts = {})
@@ -475,6 +501,16 @@ module PWN
475
501
  nil
476
502
  end
477
503
 
504
+ public_class_method def self.scope_check!(opts = {})
505
+ return nil unless defined?(Engagement)
506
+
507
+ Engagement.deny_if_out_of_scope(
508
+ args: opts[:args],
509
+ command: opts[:command] || opts[:args].inspect,
510
+ text: opts[:text]
511
+ )
512
+ end
513
+
478
514
  public_class_method def self.rfc1918?(opts = {})
479
515
  oct = opts[:ip].to_s.split('.').map(&:to_i)
480
516
  return false unless oct.length == 4
@@ -568,7 +604,9 @@ module PWN
568
604
 
569
605
  # Mint a per-thread session canary token stored on Thread.current.
570
606
  #{self}.mint_canary(
571
- bytes: 'optional - hex length (defaults to 8)'
607
+ bytes: 'optional - unused reserved length (HMAC uses 16 hex chars)',
608
+ turn: 'optional - turn number mixed into the HMAC',
609
+ session_id: 'optional - session id mixed into the HMAC'
572
610
  )
573
611
 
574
612
  # True when text contains the current session canary.
@@ -729,6 +767,13 @@ module PWN
729
767
  args: 'required - Hash of tool arguments'
730
768
  )
731
769
 
770
+ # Block network args outside the active engagement scope.
771
+ #{self}.scope_check!(
772
+ args: 'optional - Hash of tool arguments',
773
+ command: 'optional - command string to scan for hosts',
774
+ text: 'optional - free-form blob to scan'
775
+ )
776
+
732
777
  # Print the AUTHOR(S) string for this module.
733
778
  #{self}.authors
734
779
  "
@@ -227,6 +227,16 @@ module PWN
227
227
  ledger
228
228
  end
229
229
 
230
+ public_class_method def self.evidence_satisfied?(opts = {})
231
+ row = arbitrate(
232
+ request: opts[:request],
233
+ messages: opts[:messages] || opts[:trace],
234
+ paths: opts[:paths],
235
+ session_t0: opts[:session_t0]
236
+ )
237
+ row[:complete] == true && Array(row[:unmet]).empty?
238
+ end
239
+
230
240
  public_class_method def self.authors
231
241
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
232
242
  end
@@ -279,7 +289,16 @@ module PWN
279
289
 
280
290
  # Build path → {write,read} ledger from tool messages.
281
291
  #{self}.evidence_ledger(
282
- messages: 'optional - Array of role/content hashes for this turn'
292
+ messages: 'optional - Array of role/content hashes'
293
+ )
294
+
295
+ # True when write-then-readback evidence satisfies the original request.
296
+ #{self}.evidence_satisfied?(
297
+ request: 'optional - original request text',
298
+ messages: 'optional - Array of role/content hashes',
299
+ trace: 'optional - alias for messages (Array of role/content hashes)',
300
+ paths: 'optional - Array of declared deliverable paths',
301
+ session_t0: 'optional - session start Time'
283
302
  )
284
303
 
285
304
  # Print the AUTHOR(S) string for this module.
@@ -3,6 +3,7 @@
3
3
  require 'fileutils'
4
4
  require 'json'
5
5
  require 'digest'
6
+ require 'time'
6
7
 
7
8
  module PWN
8
9
  module Plugins
@@ -70,6 +71,26 @@ module PWN
70
71
  end
71
72
  end
72
73
 
74
+ public_class_method def self.put(opts = {})
75
+ bytes = opts[:bytes]
76
+ src = opts[:path].to_s
77
+ bytes = File.binread(src) if bytes.nil? && File.file?(src)
78
+ raise 'ERROR: bytes or path is required' if bytes.nil?
79
+
80
+ sha = Digest::SHA256.hexdigest(bytes)
81
+ dir = File.join(ROOT, 'sha256', sha[0, 2])
82
+ FileUtils.mkdir_p(dir)
83
+ dest = File.join(dir, sha)
84
+ File.binwrite(dest, bytes) unless File.file?(dest)
85
+ meta = File.join(ROOT, 'manifest.jsonl')
86
+ FileUtils.mkdir_p(ROOT)
87
+ File.open(meta, 'a') do |f|
88
+ f.flock(File::LOCK_EX)
89
+ f.puts(JSON.generate(sha256: sha, size: bytes.bytesize, tool: opts[:tool], session: opts[:session_id], kind: opts[:kind], created_at: Time.now.utc.iso8601, source_path: src, dest: dest))
90
+ end
91
+ { sha256: sha, path: dest, size: bytes.bytesize }
92
+ end
93
+
73
94
  public_class_method def self.authors
74
95
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
75
96
  end
@@ -81,14 +102,14 @@ module PWN
81
102
 
82
103
  # Run register and return its result
83
104
  #{self}.register(
84
- session_id: 'optional - session id value consumed by #register',
105
+ session_id: 'optional - pwn-ai session id that produced this artifact',
85
106
  path: 'required - filesystem path to read or write',
86
107
  kind: 'optional - kind value consumed by #register'
87
108
  )
88
109
 
89
110
  # Run list and return its result
90
111
  #{self}.list(
91
- session_id: 'optional - session id value consumed by #list'
112
+ session_id: 'optional - pwn-ai session id whose artifacts to list'
92
113
  )
93
114
 
94
115
  # Read an artifact file and return sha256 plus a body cap.
@@ -104,6 +125,15 @@ module PWN
104
125
  mode: 'optional - text, hex, or base64 (defaults to text)'
105
126
  )
106
127
 
128
+ # Store bytes under artifacts/sha256/<h2>/<hash> and append manifest.jsonl.
129
+ #{self}.put(
130
+ bytes: 'optional - raw bytes to store',
131
+ path: 'optional - filesystem path to read when bytes is omitted',
132
+ tool: 'optional - tool name for provenance',
133
+ session_id: 'optional - pwn-ai session id for provenance',
134
+ kind: 'optional - artifact kind'
135
+ )
136
+
107
137
  # Print the AUTHOR(S) string for this module.
108
138
  #{self}.authors
109
139
  "
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'open3'
4
+ require 'fileutils'
4
5
 
5
6
  module PWN
6
7
  module Plugins
@@ -171,6 +172,31 @@ module PWN
171
172
  end
172
173
  end
173
174
 
175
+ public_class_method def self.scaffold(opts = {})
176
+ path = (opts[:path] || opts[:bin]).to_s
177
+ raise 'ERROR: path is required' if path.empty?
178
+
179
+ triage = {}
180
+ triage = PWN::Plugins::BinaryParser.triage(path: path) if defined?(PWN::Plugins::BinaryParser)
181
+ gadgets = []
182
+ gadgets = Array(self.gadgets(path: path)[:gadgets]) if File.file?(path)
183
+ body = <<~RB
184
+ # frozen_string_literal: true
185
+ # scaffold for #{path}
186
+ # arch=#{triage[:arch]} pie=#{triage.dig(:protections, :pie)} canary=#{triage.dig(:protections, :canary)}
187
+ require 'pwn'
188
+ bin = #{path.inspect}
189
+ cyclic = PWN::Plugins::ExploitDev.cyclic(length: 200)
190
+ io = PWN::Plugins::ExploitDev.io(cmd: bin)
191
+ PWN::Plugins::ProcessTube.write_line(id: io[:id], line: cyclic)
192
+ RB
193
+ out = opts[:out].to_s
194
+ out = File.join(Dir.home, '.pwn', 'artifacts', 'scaffolds', "#{File.basename(path)}.rb") if out.empty?
195
+ FileUtils.mkdir_p(File.dirname(out))
196
+ File.write(out, body)
197
+ { path: out, triage: triage, gadgets: gadgets.first(8) }
198
+ end
199
+
174
200
  public_class_method def self.authors
175
201
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
176
202
  end
@@ -294,6 +320,13 @@ module PWN
294
320
  port: 'optional - remote TCP port'
295
321
  )
296
322
 
323
+ # Emit a ruby exploit scaffold bound to triage output.
324
+ #{self}.scaffold(
325
+ path: 'required - binary path to scaffold against',
326
+ bin: 'optional - alias for path',
327
+ out: 'optional - output .rb path (defaults under ~/.pwn/artifacts/scaffolds)'
328
+ )
329
+
297
330
  # Run gadgets and return its result
298
331
  #{self}.gadgets(
299
332
  path: 'required - filesystem path of the binary to search for gadgets'
@@ -23,6 +23,9 @@ module PWN
23
23
  arts << opts[:poc].to_s unless opts[:poc].to_s.empty?
24
24
  raise 'ERROR: poc_artifacts are required' if arts.empty?
25
25
 
26
+ ev = opts[:evidence].to_s
27
+ raise 'ERROR: evidence must be at least 40 characters citing the PoC' if ev.length < 40
28
+
26
29
  row = {
27
30
  id: SecureRandom.hex(6),
28
31
  title: title,
@@ -83,7 +86,12 @@ module PWN
83
86
  parent_id = opts[:parent_id].to_s
84
87
  raise 'ERROR: parent_id is required' if parent_id.empty?
85
88
 
86
- child = record(opts.merge(chain_parent_id: parent_id))
89
+ child = record(
90
+ opts.merge(
91
+ chain_parent_id: parent_id,
92
+ evidence: (opts[:evidence].to_s.length >= 40 ? opts[:evidence] : 'Chained finding reuses parent PoC evidence and raises composite impact.')
93
+ )
94
+ )
87
95
  ranks = { 'info' => 0, 'low' => 1, 'medium' => 2, 'high' => 3, 'critical' => 4 }
88
96
  parent = report.find { |r| r[:id].to_s == parent_id }
89
97
  sev = [parent&.[](:severity), child[:severity]].compact.max_by { |s| ranks[s.to_s] || 0 }
@@ -3,6 +3,7 @@
3
3
  require 'open3'
4
4
  require 'fileutils'
5
5
  require 'json'
6
+ require 'digest'
6
7
 
7
8
  module PWN
8
9
  module Plugins
@@ -31,12 +32,28 @@ module PWN
31
32
  end
32
33
 
33
34
  public_class_method def self.decompile(opts = {})
34
- analyze(opts)
35
+ path = (opts[:bin] || opts[:path]).to_s
36
+ raise 'ERROR: bin is required' if path.empty?
37
+
38
+ sha = Digest::SHA256.file(path).hexdigest if File.file?(path)
39
+ cache = File.join(Dir.home, '.pwn', 'cache', 'decompile', "#{sha}.json") if sha
40
+ return JSON.parse(File.read(cache), symbolize_names: true).merge(cached: true) if cache && File.file?(cache)
41
+
42
+ row = analyze(opts)
43
+ fn = opts[:function].to_s
44
+ row[:function] = fn unless fn.empty?
45
+ FileUtils.mkdir_p(File.dirname(cache)) if cache
46
+ File.write(cache, JSON.generate(row)) if cache
47
+ row
35
48
  end
36
49
 
37
50
  private_class_method def self.r2_fallback(opts = {})
38
51
  path = opts[:path].to_s
39
52
  stdout, stderr, status = Open3.capture3('r2', '-q', '-c', 'aaa;s main;pdg', path)
53
+ if stdout.to_s.strip.empty?
54
+ stdout, stderr, status = Open3.capture3('r2', '-q', '-c', 'aaa;s main;pdc', path)
55
+ return { stdout: stdout, stderr: stderr, exit: status.exitstatus, engine: 'r2-pdc' }
56
+ end
40
57
  { stdout: stdout, stderr: stderr, exit: status.exitstatus, engine: 'r2' }
41
58
  rescue StandardError => e
42
59
  { error: e.message, engine: 'r2' }
@@ -62,7 +79,8 @@ module PWN
62
79
  #{self}.decompile(
63
80
  bin: 'required - filesystem path of the binary',
64
81
  path: 'optional - alias for bin',
65
- project_dir: 'optional - Ghidra project directory'
82
+ project_dir: 'optional - Ghidra project directory',
83
+ function: 'optional - function name to decompile instead of the whole program'
66
84
  )
67
85
 
68
86
  # Print the AUTHOR(S) string for this module.
@@ -67,6 +67,23 @@ module PWN
67
67
  { ok: false, name: name, degraded: true, hint: "missing #{name}; install via pwn setup --deps" }
68
68
  end
69
69
 
70
+ CAPABILITIES = {
71
+ web_scan: %w[nuclei burpsuite zaproxy],
72
+ raw_packet: %w[],
73
+ decompile: %w[analyzeHeadless r2],
74
+ debug: %w[gdb]
75
+ }.freeze
76
+
77
+ public_class_method def self.capability_coverage(opts = {})
78
+ want = opts[:capability] || opts[:cap]
79
+ if want
80
+ bins = Array(CAPABILITIES[want.to_sym])
81
+ covered = bins.any? { |b| bin?(name: b) }
82
+ return { capability: want.to_s, covered: covered, bins: bins }
83
+ end
84
+ CAPABILITIES.keys.map { |k| capability_coverage(capability: k) }
85
+ end
86
+
70
87
  TASK_BINS = {
71
88
  'proxy' => %w[burpsuite zaproxy],
72
89
  'vuln-scan' => %w[nuclei openvas],
@@ -176,6 +193,12 @@ module PWN
176
193
  name: 'required - binary name to probe'
177
194
  )
178
195
 
196
+ # Report whether a named capability is covered by any healthy plugin binary.
197
+ #{self}.capability_coverage(
198
+ capability: 'optional - capability name such as web_scan',
199
+ cap: 'optional - alias for capability'
200
+ )
201
+
179
202
  # First healthy binary for a task (proxy, vuln-scan, re).
180
203
  #{self}.pick(
181
204
  task: 'required - capability name such as proxy or vuln-scan'
@@ -21,7 +21,7 @@ module PWN
21
21
  argv = cmd.is_a?(Array) ? cmd.map(&:to_s) : ['bash', '-lc', cmd.to_s]
22
22
  r, w, pid = PTY.spawn(*argv)
23
23
  id = "tube_#{pid}"
24
- @tubes[id] = { r: r, w: w, pid: pid, buf: +'' }
24
+ @tubes[id] = { r: r, w: w, pid: pid, buf: +'', started_at: Time.now, last_io: Time.now }
25
25
  { id: id, pid: pid }
26
26
  end
27
27
 
@@ -105,6 +105,26 @@ module PWN
105
105
  n
106
106
  end
107
107
 
108
+ public_class_method def self.list(opts = {})
109
+ _idle = opts[:idle]
110
+ now = Time.now
111
+ @tubes.map do |id, t|
112
+ { id: id, pid: t[:pid], started_at: t[:started_at], last_io: t[:last_io], age_s: (now - (t[:started_at] || now)).to_i }
113
+ end
114
+ end
115
+
116
+ public_class_method def self.kill(opts = {})
117
+ id = opts[:id].to_s
118
+ t = @tubes[id]
119
+ return { ok: false, id: id } unless t
120
+
121
+ Process.kill('TERM', t[:pid]) if t[:pid]
122
+ close(id: id)
123
+ { ok: true, id: id }
124
+ rescue StandardError => e
125
+ { ok: false, id: id, error: e.message }
126
+ end
127
+
108
128
  public_class_method def self.authors
109
129
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
110
130
  end
@@ -165,6 +185,16 @@ module PWN
165
185
  session_id: 'optional - agent session id for transcript grouping'
166
186
  )
167
187
 
188
+ # List live tubes with age.
189
+ #{self}.list(
190
+ idle: 'optional - unused reserved idle-seconds filter'
191
+ )
192
+
193
+ # SIGTERM then close a tube by id.
194
+ #{self}.kill(
195
+ id: 'required - tube id from spawn or connect'
196
+ )
197
+
168
198
  # Print the AUTHOR(S) string for this module.
169
199
  #{self}.authors
170
200
  "
data/lib/pwn/sessions.rb CHANGED
@@ -172,6 +172,10 @@ module PWN
172
172
  next unless %w[user assistant].include?(row[:role].to_s)
173
173
 
174
174
  body = row[:content].to_s
175
+ if body.include?('[compacted') && body =~ /ref=(\S+)/
176
+ ref = Regexp.last_match(1)
177
+ body = File.read(ref) if File.file?(ref)
178
+ end
175
179
  next if body.strip.empty?
176
180
  next if !target.empty? && !body.downcase.include?(target)
177
181
 
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.720'
4
+ VERSION = '0.5.721'
5
5
  end
@@ -28,6 +28,15 @@ describe PWN::AI::Agent::Dispatch do
28
28
  expect(described_class.effect(name: 'memory_recall', args: { query: 'docs' })).to eq :recall
29
29
  expect(described_class.effect(name: 'memory_remember', args: { key: 'sop' })).to eq :store
30
30
  expect(described_class.effect(name: 'learning_note_outcome', args: { task: 'x' })).to eq :store
31
+ expect(
32
+ described_class.effect(
33
+ name: 'shell',
34
+ args: {
35
+ command: "cat >> ~/.pwn/logs/pwn-ai-DEBUG-2026-09-03.md <<'EOF'\n" \
36
+ "ToolGuard, TransparentBrowser, and devtools are strengths.\nEOF"
37
+ }
38
+ )
39
+ ).to eq :write
31
40
  src = File.read(described_class.method(:effect).source_location.first)
32
41
  expect(src).to match(/def self\.effect/)
33
42
  end
@@ -985,6 +985,50 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
985
985
  Thread.current[:pwn_loop_deliverables] = nil
986
986
  end
987
987
 
988
+ it 'does not bounce with empty unmet after a redirected write whose body mentions browsers' do
989
+ Dir.mktmpdir do |dir|
990
+ path = File.join(dir, 'pwn-ai-DEBUG-2026-09-03.md')
991
+ File.write(path, "# assessment\n" * 40)
992
+ req = "analyze how well pwn-ai enables pentest. Store this list in #{path}."
993
+ write_cmd = "cat >> #{path} <<'EOF'\nTransparentBrowser and devtools are strengths.\nEOF"
994
+ msgs = [
995
+ { role: 'user', content: req },
996
+ {
997
+ role: 'assistant',
998
+ tool_calls: [{ function: { name: 'shell', arguments: { command: write_cmd }.to_json } }]
999
+ },
1000
+ {
1001
+ role: 'tool',
1002
+ name: 'shell',
1003
+ content: {
1004
+ success: true,
1005
+ result: { stdout: 'APPENDED', exit: 0 },
1006
+ effect: PWN::AI::Agent::Dispatch.effect(name: 'shell', args: { command: write_cmd })
1007
+ }.to_json
1008
+ },
1009
+ {
1010
+ role: 'assistant',
1011
+ tool_calls: [{ function: { name: 'shell', arguments: { command: "cat #{path}" }.to_json } }]
1012
+ },
1013
+ {
1014
+ role: 'tool',
1015
+ name: 'shell',
1016
+ content: { success: true, result: { stdout: File.read(path), exit: 0 }, effect: 'read' }.to_json
1017
+ }
1018
+ ]
1019
+ expect(described_class.send(:completion_unmet, request: req, messages: msgs)).to eq([])
1020
+ expect(described_class.send(:request_unsatisfied?, request: req, messages: msgs)).to eq(false)
1021
+ expect(
1022
+ described_class.send(
1023
+ :may_finalize?,
1024
+ request: req,
1025
+ messages: msgs,
1026
+ text: "Done. The assessment is at #{path}."
1027
+ )
1028
+ ).to eq(true)
1029
+ end
1030
+ end
1031
+
988
1032
  it 'keeps issue_work unsatisfied until proofs exist, without Crit/High regex' do
989
1033
  src = File.read(described_class.method(:run).source_location.first)
990
1034
  expect(src).not_to include('Crit/High')
@@ -411,7 +411,7 @@ describe 'PWN::AI::Agent::Reward vs TUI plan' do
411
411
 
412
412
  it 'floors verified PASS analytical answers at 0.6' do
413
413
  src = File.read(PWN::AI::Agent::Reward.method(:judge).source_location.first)
414
- expect(src).to include('[score, 0.7].max')
414
+ expect(src).to include('[score, 0.6].max')
415
415
  expect(src).to include('\bPASS\b')
416
416
  end
417
417
 
@@ -36,6 +36,15 @@ describe PWN::AI::Agent::ToolGuard do
36
36
  it 'rejects a real command' do
37
37
  expect(described_class.placeholder?(text: 'uname -r')).to be false
38
38
  end
39
+
40
+ it 'does not treat ellipsis inside a heredoc body as a placeholder command' do
41
+ body = "cat <<'EOF'\nThe token ... lives in the document body.\nEOF\n"
42
+ expect(described_class.placeholder?(text: body)).to be false
43
+ end
44
+
45
+ it 'still denies a command that is only a placeholder token' do
46
+ expect(described_class.placeholder?(text: ' ... ')).to be true
47
+ end
39
48
  end
40
49
 
41
50
  describe '.coerce_args' do
@@ -7,7 +7,7 @@ describe PWN::Plugins::Findings do
7
7
  it 'records and reports a finding' do
8
8
  Dir.mktmpdir do |dir|
9
9
  stub_const('PWN::Plugins::Findings::FILE', File.join(dir, 'findings.jsonl'))
10
- row = described_class.record(title: 'xss', severity: 'high', host: '127.0.0.1', poc: '/tmp/poc.rb')
10
+ row = described_class.record(title: 'xss', severity: 'high', host: '127.0.0.1', poc: '/tmp/poc.rb', evidence: 'PoC at /tmp/poc.rb reproduces XSS in search with a reflected payload.')
11
11
  expect(row[:id]).not_to be_empty
12
12
  expect(described_class.report.first[:title]).to eq('xss')
13
13
  end
@@ -19,7 +19,7 @@ describe PWN::Plugins::Findings do
19
19
  expect { described_class.record(title: 'xss') }.to raise_error(/poc/i)
20
20
  poc = File.join(dir, 'poc.rb')
21
21
  File.write(poc, 'puts 1')
22
- row = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'high')
22
+ row = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'high', evidence: 'PoC file reproduces reflected XSS in search; see artifact path.')
23
23
  expect(row[:poc_artifacts]).to include(poc)
24
24
  out = described_class.render(dir_path: dir, report_name: 'findings')
25
25
  expect(File.file?(out[:markdown])).to be true
@@ -33,7 +33,7 @@ describe PWN::Plugins::Findings do
33
33
  stub_const('PWN::Plugins::Findings::FILE', File.join(dir, 'findings.jsonl'))
34
34
  poc = File.join(dir, 'poc.rb')
35
35
  File.write(poc, 'puts 1')
36
- parent = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'medium')
36
+ parent = described_class.record(title: 'xss', poc_artifacts: [poc], severity: 'medium', evidence: 'PoC file reproduces reflected XSS in search; see artifact path.')
37
37
  child = described_class.chain(parent_id: parent[:id], title: 'account-takeover', poc_artifacts: [poc], severity: 'high')
38
38
  expect(child[:chain_parent_id]).to eq(parent[:id])
39
39
  expect(child[:composite_severity]).to eq('high')
@@ -46,7 +46,7 @@ describe PWN::Plugins::Findings do
46
46
  stub_const('PWN::Plugins::Findings::FILE', File.join(dir, 'findings.jsonl'))
47
47
  poc = File.join(dir, 'poc.rb')
48
48
  File.write(poc, 'puts 1')
49
- described_class.record(title: 'xss', poc_artifacts: [poc], engagement_id: 'lab')
49
+ described_class.record(title: 'xss', poc_artifacts: [poc], engagement_id: 'lab', evidence: 'PoC file reproduces reflected XSS in search; see artifact path.')
50
50
  expect(described_class.evidence_verify(engagement_id: 'lab')[:ok]).to eq(true)
51
51
  File.write(poc, 'tampered')
52
52
  expect(described_class.evidence_verify(engagement_id: 'lab')[:ok]).to eq(false)
@@ -374,6 +374,7 @@
374
374
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.escalate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.escalate`: "}]}
375
375
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_blob Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_blob`: "}]}
376
376
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_enough_to_finalize? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_enough_to_finalize?`: "}]}
377
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_satisfied? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_satisfied?`: "}]}
377
378
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_tokens_missing? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_tokens_missing?`: "}]}
378
379
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.expose_current_session Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.expose_current_session`: "}]}
379
380
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.extract_recall_match Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.extract_recall_match`: "}]}
@@ -439,6 +440,7 @@
439
440
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_or_read_evidenced? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_or_read_evidenced?`: "}]}
440
441
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_verified? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_verified?`: "}]}
441
442
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.advantage Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.advantage`: Supported Method Parameters\n\na = PWN::AI::Agent::Metrics.advantage(name: ‘shell’)\n\nC1 — tool.success_rate − global_rate over the rolling window.\n"}]}
443
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.append_jsonl Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.append_jsonl`: "}]}
442
444
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
443
445
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.beta_sample Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.beta_sample`: "}]}
444
446
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.blank_bucket Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.blank_bucket`: "}]}
@@ -602,6 +604,7 @@
602
604
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.definitions Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.definitions`: Supported Method Parameters\n\ntools = PWN::AI::Agent::Registry.definitions(\n\nenabled: 'optional - Array of toolset names to include; nil = all whose check passes',\nrelevance: 'optional - user request; when set AND :tool_router is enabled, slim to CORE + top-K keyword matches',\ntop_k: 'optional - keyword-ranked tools to keep beyond CORE (default 10)',\norder: 'optional - preference list forwarded only when this key is present',\npreference: 'optional - alias of :order; same key-present rule'\n\n)\n"}]}
603
605
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.discover Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.discover`: Supported Method Parameters\n\nnames = PWN::AI::Agent::Registry.discover(\n\nforce: 'optional - re-require tool files even if already discovered (default false)'\n\n)\n"}]}
604
606
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.domain_pin_names Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.domain_pin_names`: "}]}
607
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.eager_load! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.eager_load!`: "}]}
605
608
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.help`: "}]}
606
609
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.lookup Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.lookup`: Supported Method Parameters\n\nentry = PWN::AI::Agent::Registry.lookup(\n\nname: 'required - registered tool name'\n\n)\n"}]}
607
610
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.metrics_rates Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.metrics_rates`: "}]}
@@ -610,6 +613,7 @@
610
613
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.register Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.register`: Supported Method Parameters\n\nPWN::AI::Agent::Registry.register(\n\nname: 'required - tool name exposed to the model',\ntoolset: 'required - grouping for enable/disable (terminal, file, pwn, memory…)',\nschema: 'required - OpenAI function schema {name:, description:, parameters:}',\nhandler: 'required - ->(args_hash) { ... } returning a JSON-serialisable object',\ncheck: 'optional - -> { bool } gate; tool only advertised when truthy',\nmax_chars: 'optional - cap on serialised result (default 24_000)'\n\n)\n"}]}
611
614
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.router_enabled? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.router_enabled?`: "}]}
612
615
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.safe_check Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.safe_check`: "}]}
616
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.selftest Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.selftest`: "}]}
613
617
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.toolsets Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.toolsets`: Supported Method Parameters\n\nnames = PWN::AI::Agent::Registry.toolsets\n"}]}
614
618
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Result.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Result.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
615
619
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Result.collect Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Result.collect`: "}]}
@@ -807,6 +811,7 @@
807
811
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.reset_timeout_budget Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.reset_timeout_budget`: "}]}
808
812
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.reset_timeout_budget! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.reset_timeout_budget!`: "}]}
809
813
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.rfc1918? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.rfc1918?`: "}]}
814
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.scope_check! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.scope_check!`: "}]}
810
815
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.scope_refusal Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.scope_refusal`: "}]}
811
816
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.shell_bash? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.shell_bash?`: "}]}
812
817
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.shell_name Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.shell_name`: "}]}
@@ -827,6 +832,7 @@
827
832
  {"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.defer Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.defer`: Supported Method Parameters\n\nresult = PWN::AI::Agent::TurnFinalizer.defer(\n\nsession_id: 'required - PWN::Sessions id',\nrequest: 'optional - original user request',\nfinal: 'optional - user-visible final answer',\npredicted: 'optional - W3 predicted score',\nplan: 'optional - tangible-task plan',\nts_state: 'optional - TaskSummarizer state'\n\n)\n\nSnapshots the live Policy episode onto this thread, clears it so Loop.maybe_finish_policy is a no-op, and runs Learning.auto_introspect on a daemon thread with the episode re-attached.\n"}]}
828
833
  {"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.enter_user_path! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.enter_user_path!`: Supported Method Parameters\n\nPWN::AI::Agent::TurnFinalizer.enter_user_path!\n"}]}
829
834
  {"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.evidence_ledger Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.evidence_ledger`: "}]}
835
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.evidence_satisfied? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.evidence_satisfied?`: "}]}
830
836
  {"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.finalize Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.finalize`: Supported Method Parameters\n\nresult = PWN::AI::Agent::TurnFinalizer.finalize(\n\nsession_id:, request:, final:, predicted:, plan:, ts_state:,\nshould: true|false\n\n)\n\nSingle entry used when a caller wants the Hermes split without going through Learning.auto_introspect‘s gate.\n"}]}
831
837
  {"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.help`: "}]}
832
838
  {"messages":[{"role":"user","content":"PWN::AI::Agent::TurnFinalizer.join_all! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TurnFinalizer.join_all!`: Supported Method Parameters\n\nn = PWN::AI::Agent::TurnFinalizer.join_all!(timeout: 15)\n"}]}
@@ -1804,6 +1810,7 @@
1804
1810
  {"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.get Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.get`: "}]}
1805
1811
  {"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.help Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.help`: "}]}
1806
1812
  {"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.list Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.list`: "}]}
1813
+ {"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.put Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.put`: "}]}
1807
1814
  {"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.read_page Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.read_page`: "}]}
1808
1815
  {"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.register Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.register`: "}]}
1809
1816
  {"messages":[{"role":"user","content":"PWN::Plugins::ArtifactRegistry.required_bins Usage"},{"role":"assistant","content":"`PWN::Plugins::ArtifactRegistry.required_bins`: "}]}
@@ -2072,6 +2079,7 @@
2072
2079
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.p8 Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.p8`: "}]}
2073
2080
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.pack_int Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.pack_int`: "}]}
2074
2081
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.required_bins Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.required_bins`: "}]}
2082
+ {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.scaffold Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.scaffold`: "}]}
2075
2083
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.shellcode Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.shellcode`: "}]}
2076
2084
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.u16 Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.u16`: "}]}
2077
2085
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.u32 Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.u32`: "}]}
@@ -2515,6 +2523,7 @@
2515
2523
  {"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.authors`: "}]}
2516
2524
  {"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.bin? Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.bin?`: "}]}
2517
2525
  {"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.cap_net_raw? Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.cap_net_raw?`: "}]}
2526
+ {"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.capability_coverage Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.capability_coverage`: "}]}
2518
2527
  {"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.check Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.check`: "}]}
2519
2528
  {"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.help Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.help`: "}]}
2520
2529
  {"messages":[{"role":"user","content":"PWN::Plugins::PreflightChecker.host_summary Usage"},{"role":"assistant","content":"`PWN::Plugins::PreflightChecker.host_summary`: "}]}
@@ -2529,6 +2538,8 @@
2529
2538
  {"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.connect Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.connect`: "}]}
2530
2539
  {"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.expect Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.expect`: "}]}
2531
2540
  {"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.help Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.help`: "}]}
2541
+ {"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.kill Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.kill`: "}]}
2542
+ {"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.list Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.list`: "}]}
2532
2543
  {"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.reap_orphans Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.reap_orphans`: "}]}
2533
2544
  {"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.recvline Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.recvline`: "}]}
2534
2545
  {"messages":[{"role":"user","content":"PWN::Plugins::ProcessTube.recvuntil Usage"},{"role":"assistant","content":"`PWN::Plugins::ProcessTube.recvuntil`: "}]}
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.720
4
+ version: 0.5.721
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.