pwn 0.5.721 → 0.5.722

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: c26adc03f68a0e0275c0eb1a85aa2bb06def95aeb014411b000f52c6a83e3e40
4
- data.tar.gz: 6a082b81f8f1885de1b3ac42c604f1f21a08d48e7260609ce35e00d1461afa79
3
+ metadata.gz: 31f54d1cd6a7bfbc4cef3bcc989650930b1b31bbf2bbc47654b20d6f20158775
4
+ data.tar.gz: 50bfda31eecd4b99d17324a8c4c548f14b8df10561c183f27bc9e7157c8f88bf
5
5
  SHA512:
6
- metadata.gz: 719d713266b6a3d4200895bb41d4ac5ec7ccfc6d142463cb70085cd6c8d02a514191553b5b12317de45a38abf3d4d1ccb47101f54dc5e588e596a13bc3e77af8
7
- data.tar.gz: 271d9ff938ac37275ebe9da7d3031305bd09f587e5b6d14f0386fc006bd120b6e684b00aa651c935da7eefbd3a71c298b114ef07ec21b7a9099a75d9f71df647
6
+ metadata.gz: 3a43118f36da1f9712dbfb908b5f0c9d4650d3bfde63239240d7c9cefcc50d29d78468e14e9cd9bf44f9fdba5df971b48659b348619dc79a7e17667028930025
7
+ data.tar.gz: 867d72ec34f85caaf18fa5abeb38b466d5663c1361becf17cb4af87115dd0d45411527b0f5616dd54874c241544416fb0b9427f16476248ed38a864b3951daef
@@ -34,6 +34,8 @@ PWN::AI::Agent::Learning.note_outcome(opts)
34
34
  ## Public methods
35
35
 
36
36
  - `note_outcome`
37
+ - `consistency_check`
38
+ - `disputed_save`
37
39
  - `outcomes`
38
40
  - `stats`
39
41
  - `to_context`
@@ -38,6 +38,7 @@ PWN::AI::Agent::Metrics.load(opts)
38
38
  - `record`
39
39
  - `append_jsonl`
40
40
  - `summary`
41
+ - `snapshot`
41
42
  - `to_context`
42
43
  - `proxy_trust`
43
44
  - `ucb`
@@ -37,6 +37,7 @@ PWN::AI::Agent::Mistakes.load(opts)
37
37
  - `save`
38
38
  - `signature`
39
39
  - `error_class`
40
+ - `family`
40
41
  - `find`
41
42
  - `for_tool`
42
43
  - `record`
@@ -48,6 +49,7 @@ PWN::AI::Agent::Mistakes.load(opts)
48
49
  - `operator_inbox`
49
50
  - `to_context`
50
51
  - `correction_hint`
52
+ - `note_hint_outcome`
51
53
  - `correction`
52
54
  - `check_user_correction`
53
55
  - `lean`
@@ -53,6 +53,7 @@ PWN::Plugins::ExploitDev.required_bins(opts)
53
53
  - `fmtstr`
54
54
  - `io`
55
55
  - `scaffold`
56
+ - `from_crash`
56
57
  - `authors`
57
58
  - `help`
58
59
 
@@ -42,6 +42,7 @@ PWN::Plugins::GDB.required_bins(opts)
42
42
  - `breakpoints`
43
43
  - `crash_info`
44
44
  - `debug_session`
45
+ - `ptrace_preflight`
45
46
  - `authors`
46
47
  - `help`
47
48
 
@@ -35,6 +35,7 @@ PWN::Plugins::Jobs.required_bins(opts)
35
35
 
36
36
  - `required_bins`
37
37
  - `start`
38
+ - `watch`
38
39
  - `status`
39
40
  - `tail`
40
41
  - `result`
@@ -72,6 +72,15 @@ module PWN
72
72
  error: 'refused: memory_remember/skills_update text copied from last tool output'
73
73
  )
74
74
  end
75
+ if blob.match?(/open_sockraw|sockraw/) && defined?(PWN::Plugins::PreflightChecker) &&
76
+ PWN::Plugins::PreflightChecker.respond_to?(:cap_net_raw?) && !PWN::Plugins::PreflightChecker.cap_net_raw?
77
+ return JSON.generate(
78
+ success: false,
79
+ error: 'capability missing CAP_NET_RAW',
80
+ substitute: 'PWN::Plugins::Packet.tcp_connect_scan',
81
+ code: 'CAP_DENY'
82
+ )
83
+ end
75
84
  result = entry.handler.call(args)
76
85
  result = ToolGuard.quarantine_output(text: result) if defined?(ToolGuard) && result.is_a?(String) && ToolGuard.respond_to?(:quarantine_output)
77
86
  note_taint(text: result)
@@ -26,6 +26,7 @@ module PWN
26
26
  # restarts and is shared by every future session.
27
27
  module Learning
28
28
  LEARNING_FILE = File.join(Dir.home, '.pwn', 'learning.jsonl')
29
+ DISPUTED_FILE = File.join(Dir.home, '.pwn', 'learning', 'disputed.jsonl')
29
30
  LESSONS_FILE = File.join(Dir.home, '.pwn', 'lessons.json')
30
31
  FINETUNE_DIR = File.join(Dir.home, '.pwn', 'finetune')
31
32
  # P0 — post-answer introspect must not train "stop early" while
@@ -138,6 +139,12 @@ module PWN
138
139
  entry[:success] = true
139
140
  success = true
140
141
  end
142
+ check = consistency_check(details: details, success: success, rationale: opts[:rationale])
143
+ if check == :disputed
144
+ entry[:status] = 'disputed'
145
+ disputed_save(entry: entry)
146
+ return entry
147
+ end
141
148
  FileUtils.mkdir_p(File.dirname(LEARNING_FILE))
142
149
  File.open(LEARNING_FILE, 'a') { |f| f.puts(JSON.generate(entry)) }
143
150
  maybe_prune_outcomes!
@@ -159,6 +166,22 @@ module PWN
159
166
  entry
160
167
  end
161
168
 
169
+ public_class_method def self.consistency_check(opts = {})
170
+ details = "#{opts[:details]} #{opts[:rationale]}"
171
+ success = opts[:success]
172
+ return :disputed if details.match?(/\bPASS\b/) && success == false
173
+ return :disputed if details.match?(/\bFAIL\b/) && success == true && !details.match?(/\bPASS\b/)
174
+
175
+ :ok
176
+ end
177
+
178
+ public_class_method def self.disputed_save(opts = {})
179
+ entry = opts[:entry] || {}
180
+ FileUtils.mkdir_p(File.dirname(DISPUTED_FILE))
181
+ File.open(DISPUTED_FILE, 'a') { |f| f.puts(JSON.generate(entry)) }
182
+ entry
183
+ end
184
+
162
185
  # Supported Method Parameters::
163
186
  # rows = PWN::AI::Agent::Learning.outcomes(
164
187
  # limit: 'optional - max entries returned newest-first (default 50)',
@@ -2081,6 +2104,18 @@ module PWN
2081
2104
  include_demoted: 'optional - include demoted lessons (defaults to false)'
2082
2105
  )
2083
2106
 
2107
+ # Refuse PASS+success:false rows; they go to the disputed queue.
2108
+ #{self}.consistency_check(
2109
+ details: 'optional - details string that may contain PASS/FAIL',
2110
+ rationale: 'optional - judge rationale text',
2111
+ success: 'required - boolean success flag'
2112
+ )
2113
+
2114
+ # Append a disputed outcome to ~/.pwn/learning/disputed.jsonl.
2115
+ #{self}.disputed_save(
2116
+ entry: 'required - Hash of the disputed learning row'
2117
+ )
2118
+
2084
2119
  # Print the AUTHOR(S) string for this module.
2085
2120
  #{self}.authors
2086
2121
  "
@@ -1176,6 +1176,16 @@ module PWN
1176
1176
  false
1177
1177
  end
1178
1178
 
1179
+ private_class_method def self.session_tool_budget(opts = {})
1180
+ name = opts[:name].to_s
1181
+ result = opts[:result].to_s
1182
+ return 16_384 if name.match?(/decompile|binary_triage|artifact_read|exploitdev/)
1183
+ return 16_384 if name == 'pwn_eval' && result.match?(/r2 |objdump|xxd |pdf\b|afl-/)
1184
+ return 16_384 if name == 'shell' && result.match?(/objdump|xxd|radare|r2 |disassembl/)
1185
+
1186
+ 1_024
1187
+ end
1188
+
1179
1189
  # E1 — did the environment change under this tool? If Metrics CUSUM
1180
1190
  # tripped for it in the last hour AND Extrospection.drift shows a
1181
1191
  # toolchain/net/repo change, blame the WORLD not the AGENT.
@@ -3126,7 +3136,7 @@ module PWN
3126
3136
  append_session(
3127
3137
  session_id: session_id,
3128
3138
  role: 'tool',
3129
- content: "#{name} → #{result[0, 1_024]}"
3139
+ content: "#{name} → #{result[0, session_tool_budget(name: name, result: result)]}"
3130
3140
  )
3131
3141
  end
3132
3142
 
@@ -156,6 +156,19 @@ module PWN
156
156
  .sort_by { |r| [-r[:calls], -r[:success_rate]] }.first(limit)
157
157
  end
158
158
 
159
+ public_class_method def self.snapshot(opts = {})
160
+ _day = opts[:day]
161
+ learn = defined?(Learning) ? Learning.stats : {}
162
+ {
163
+ success_rate: learn[:success_rate],
164
+ success_rate_orm: learn[:success_rate_orm],
165
+ success_rate_heur: learn[:success_rate_heur],
166
+ brier: (learn.dig(:calibration, :brier) if learn.is_a?(Hash)),
167
+ proxy_distrust: learn[:proxy_distrust],
168
+ tools: summary(limit: 8)
169
+ }
170
+ end
171
+
159
172
  # Supported Method Parameters::
160
173
  # ctx = PWN::AI::Agent::Metrics.to_context(
161
174
  # limit: 'optional - cap number of tools included (default 8)',
@@ -814,6 +827,11 @@ module PWN
814
827
  # Run reset and return its result
815
828
  #{self}.reset
816
829
 
830
+ # Snapshot success rates per judge source for the LEARNING block.
831
+ #{self}.snapshot(
832
+ day: 'optional - reserved day key for rotation'
833
+ )
834
+
817
835
  # Print the AUTHOR(S) string for this module.
818
836
  #{self}.authors
819
837
  "
@@ -127,6 +127,31 @@ module PWN
127
127
  'other'
128
128
  end
129
129
 
130
+ public_class_method def self.family(opts = {})
131
+ klass = error_class(opts)
132
+ case klass
133
+ when 'docker_registry_auth', 'auth_denied' then 'auth_denied'
134
+ when 'name_conflict' then 'name_conflict'
135
+ when 'parse_error' then 'parse_error'
136
+ when 'missing_path', 'enoent' then 'missing_path'
137
+ when 'perm_denied', 'socket_perm', 'eacces' then 'permission_capability'
138
+ when 'net_unreach' then 'network_unreachable'
139
+ when 'timeout' then 'timeout'
140
+ else 'other'
141
+ end
142
+ end
143
+
144
+ FAMILY_FIXES = {
145
+ 'auth_denied' => 'check registry credentials / image name; do NOT ls paths',
146
+ 'name_conflict' => 'rename the resource or remove the existing name; do NOT ls paths',
147
+ 'parse_error' => 'fix template/JSON syntax; do NOT ls paths',
148
+ 'missing_path' => 'Path missing. ls/test -e the parent first, then run. Do not retry the same missing path.',
149
+ 'permission_capability' => 'Raw sockets need CAP_NET_RAW. Do not retry open_sockraw. Use connect-scan or Sock.',
150
+ 'network_unreachable' => 'network unreachable; check routing/VPN, do not ls paths',
151
+ 'timeout' => 'deadline too short; retry same payload with timeout += 180',
152
+ 'other' => 'inspect the exact error class before retrying'
153
+ }.freeze
154
+
130
155
  # Supported Method Parameters::
131
156
  # entry = PWN::AI::Agent::Mistakes.find(
132
157
  # signature: 'optional - exact signature to fetch',
@@ -630,10 +655,38 @@ module PWN
630
655
 
631
656
  parts = ["seen #{m[:count]}× across #{Array(m[:sessions]).length} session(s), sig=#{m[:signature]}"]
632
657
  parts << 'REGRESSED (previous fix did not hold)' if m[:regressed]
633
- parts << "KNOWN FIX: #{m[:fix]}" if m[:fix].to_s.strip.length.positive?
658
+ fam = family(error: opts[:error] || m[:error])
659
+ fam_fix = FAMILY_FIXES[fam]
660
+ stored = m[:fix].to_s.strip
661
+ stored = '' if stored.match?(%r{ls/test -e the parent}) && fam != 'missing_path'
662
+ stored = '' if m[:hint_confidence].to_f.negative?
663
+ stored = '' if stored.include?('[UNVERIFIED]')
664
+ hint = stored.empty? ? fam_fix : stored
665
+ parts << "KNOWN FIX: #{hint}" unless hint.to_s.empty?
634
666
  "[pwn-ai/mistakes] #{parts.join(' | ')}"
635
667
  end
636
668
 
669
+ public_class_method def self.note_hint_outcome(opts = {})
670
+ sig = (opts[:signature] || signature(tool: opts[:tool], error: opts[:error])).to_s
671
+ return nil if sig.empty?
672
+
673
+ store = load
674
+ m = store[sig.to_sym]
675
+ return nil unless m
676
+
677
+ m[:hint_outcomes] = m[:hint_outcomes].to_i + 1
678
+ unless opts[:helped]
679
+ m[:hint_misses] = m[:hint_misses].to_i + 1
680
+ if m[:hint_misses].to_i >= 3
681
+ m[:fix] = "[UNVERIFIED] #{m[:fix]}"
682
+ m[:hint_confidence] = -1.0
683
+ end
684
+ end
685
+ store[sig.to_sym] = m
686
+ save(store: store)
687
+ m
688
+ end
689
+
637
690
  # Supported Method Parameters::
638
691
  # bool = PWN::AI::Agent::Mistakes.correction?(request: user_text)
639
692
 
@@ -988,6 +1041,19 @@ module PWN
988
1041
  signature: 'optional - signature value consumed by #effective_count'
989
1042
  )
990
1043
 
1044
+ # Map an error string onto a coarse family (auth vs path vs parse).
1045
+ #{self}.family(
1046
+ error: 'required - raw error text'
1047
+ )
1048
+
1049
+ # Record whether an injected KNOWN FIX helped; demote after 3 misses.
1050
+ #{self}.note_hint_outcome(
1051
+ tool: 'optional - tool name',
1052
+ error: 'optional - raw error text',
1053
+ signature: 'optional - explicit signature',
1054
+ helped: 'optional - true when the hint resolved the failure'
1055
+ )
1056
+
991
1057
  # Print the AUTHOR(S) string for this module.
992
1058
  #{self}.authors
993
1059
  "
@@ -143,17 +143,21 @@ module PWN
143
143
  # confidence handles that in the bandit blend.
144
144
  eng = (PWN::Env.dig(:ai, :active) if defined?(PWN::Env)).to_s.downcase
145
145
  local = eng == 'ollama' || eng.empty?
146
- if v[:source].to_s == 'heuristic'
147
- v[:confidence] = local ? 0.35 : 0.5
146
+ if v[:source].to_s == 'heuristic' || v[:source].to_s.start_with?('heuristic')
147
+ toolbacked = Array(trace).any?
148
+ v[:heuristic_class] = toolbacked ? :toolbacked : :textual
149
+ v[:source] = :heuristic
150
+ v[:confidence] = if toolbacked
151
+ local ? 0.55 : 0.7
152
+ else
153
+ local ? 0.35 : 0.5
154
+ end
148
155
  raw = v[:score].to_f
149
156
  v[:score_raw] = raw
150
- # preserve empty / failure-language / polite floors exactly (raw <= 0.15)
151
- # and pass-through non-capped heuristics via the default arm.
152
- if local && raw > 0.15 && trace.empty? && raw >= 0.6 && final.length < 400
157
+ if !toolbacked && local && raw > 0.15 && trace.empty? && raw >= 0.6 && final.length < 400
153
158
  v[:score] = [raw, 0.45].min
154
159
  v[:rationale] = "#{v[:rationale]} | P1:local_no_trace_cap"
155
- elsif local && raw > 0.15 && trace.length < 2 && raw >= 0.85
156
- # thin-evidence local highs: mild shrink toward 0.5
160
+ elsif !toolbacked && local && raw > 0.15 && trace.length < 2 && raw >= 0.85
157
161
  v[:score] = (0.5 + ((raw - 0.5) * 0.7)).round(3).clamp(0.0, 1.0)
158
162
  else
159
163
  v[:score] = raw
@@ -45,17 +45,22 @@ PWN::AI::Agent::Registry.register(
45
45
  path: { type: 'string' },
46
46
  offset: { type: 'integer' },
47
47
  length: { type: 'integer' },
48
- mode: { type: 'string', description: 'text|hex|base64' }
48
+ mode: { type: 'string', description: 'text|hex|base64' },
49
+ grep: { type: 'string' },
50
+ ref: { type: 'string' },
51
+ sha256: { type: 'string' }
49
52
  },
50
53
  required: %w[path]
51
54
  }
52
55
  },
53
56
  handler: lambda { |args|
54
57
  PWN::Plugins::ArtifactRegistry.read_page(
55
- path: args[:path] || args['path'],
58
+ path: args[:path] || args['path'] || args[:ref] || args['ref'],
56
59
  offset: args[:offset] || args['offset'],
57
60
  length: args[:length] || args['length'],
58
- mode: args[:mode] || args['mode']
61
+ mode: args[:mode] || args['mode'],
62
+ grep: args[:grep] || args['grep'],
63
+ sha256: args[:sha256] || args['sha256']
59
64
  )
60
65
  }
61
66
  )
@@ -51,13 +51,26 @@ module PWN
51
51
  end
52
52
 
53
53
  public_class_method def self.read_page(opts = {})
54
- path = opts[:path].to_s
54
+ path = (opts[:path] || opts[:ref]).to_s
55
55
  raise 'ERROR: path is required' if path.empty?
56
56
  raise "ERROR: file not found: #{path}" unless File.file?(path)
57
57
 
58
+ raise 'ERROR: sha256 mismatch' if opts[:sha256].to_s != '' && Digest::SHA256.file(path).hexdigest != opts[:sha256].to_s
59
+
60
+ grep = opts[:grep].to_s
61
+ unless grep.empty?
62
+ rx = Regexp.new(grep, Regexp::IGNORECASE)
63
+ hits = []
64
+ File.foreach(path).with_index(1) do |ln, i|
65
+ hits << { line: i, text: ln.chomp } if ln.match?(rx)
66
+ break if hits.length >= 50
67
+ end
68
+ return { path: path, grep: grep, matches: hits }
69
+ end
70
+
58
71
  offset = opts[:offset].to_i
59
- length = (opts[:length] || 4_096).to_i
60
- length = 4_096 if length <= 0
72
+ length = (opts[:length] || 16_384).to_i
73
+ length = 16_384 if length <= 0
61
74
  mode = (opts[:mode] || 'text').to_s
62
75
  data = File.binread(path, length, offset).to_s
63
76
  case mode
@@ -117,14 +130,6 @@ module PWN
117
130
  path: 'required - filesystem path of a registered artifact'
118
131
  )
119
132
 
120
- # Page an artifact as text, hex, or base64 from an offset.
121
- #{self}.read_page(
122
- path: 'required - filesystem path of the artifact',
123
- offset: 'optional - byte offset (defaults to 0)',
124
- length: 'optional - byte count (defaults to 4096)',
125
- mode: 'optional - text, hex, or base64 (defaults to text)'
126
- )
127
-
128
133
  # Store bytes under artifacts/sha256/<h2>/<hash> and append manifest.jsonl.
129
134
  #{self}.put(
130
135
  bytes: 'optional - raw bytes to store',
@@ -134,6 +139,17 @@ module PWN
134
139
  kind: 'optional - artifact kind'
135
140
  )
136
141
 
142
+ # Page an artifact; grep: searches lines, ref: alias for path.
143
+ #{self}.read_page(
144
+ path: 'required - filesystem path of the artifact',
145
+ ref: 'optional - alias for path',
146
+ offset: 'optional - byte offset (defaults to 0)',
147
+ length: 'optional - byte count (defaults to 16384)',
148
+ mode: 'optional - text, hex, or base64 (defaults to text)',
149
+ grep: 'optional - regex to search instead of paging',
150
+ sha256: 'optional - expected sha256 of the file'
151
+ )
152
+
137
153
  # Print the AUTHOR(S) string for this module.
138
154
  #{self}.authors
139
155
  "
@@ -120,7 +120,10 @@ module PWN
120
120
  end
121
121
  body = summary.merge(path: path, sha256: sha)
122
122
  File.write(art, JSON.pretty_generate(body))
123
- body.merge(artifact: art, cached: false)
123
+ facts_dir = File.join(Dir.home, '.pwn', 'engagements', 'default', 'binaries', sha)
124
+ FileUtils.mkdir_p(facts_dir)
125
+ File.write(File.join(facts_dir, 'facts.json'), JSON.pretty_generate(body))
126
+ body.merge(artifact: art, cached: false, facts: File.join(facts_dir, 'facts.json'))
124
127
  end
125
128
 
126
129
  public_class_method def self.diff(opts = {})
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'open3'
4
4
  require 'fileutils'
5
+ require 'json'
5
6
 
6
7
  module PWN
7
8
  module Plugins
@@ -197,6 +198,14 @@ module PWN
197
198
  { path: out, triage: triage, gadgets: gadgets.first(8) }
198
199
  end
199
200
 
201
+ public_class_method def self.from_crash(opts = {})
202
+ path = (opts[:path] || opts[:from_crash]).to_s
203
+ raise 'ERROR: path is required' if path.empty?
204
+ raise "ERROR: file not found: #{path}" unless File.file?(path)
205
+
206
+ JSON.parse(File.read(path), symbolize_names: true)
207
+ end
208
+
200
209
  public_class_method def self.authors
201
210
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
202
211
  end
@@ -344,6 +353,12 @@ module PWN
344
353
  names: 'optional - Array of symbol names (defaults to system, execve, __libc_start_main)'
345
354
  )
346
355
 
356
+ # Load a crash.json produced by a fuzz campaign.
357
+ #{self}.from_crash(
358
+ path: 'required - filesystem path of crash.json',
359
+ from_crash: 'optional - alias for path'
360
+ )
361
+
347
362
  # Print the AUTHOR(S) string for this module.
348
363
  #{self}.authors
349
364
  "
@@ -90,6 +90,18 @@ module PWN
90
90
  crash_info(opts.merge(commands: script))
91
91
  end
92
92
 
93
+ public_class_method def self.ptrace_preflight(opts = {})
94
+ _target = opts[:target]
95
+ scope = 0
96
+ path = '/proc/sys/kernel/yama/ptrace_scope'
97
+ scope = File.read(path).to_i if File.file?(path)
98
+ {
99
+ ptrace_scope: scope,
100
+ attach_ok: scope.zero?,
101
+ hint: scope.positive? ? 'spawn instead of attach' : 'ok'
102
+ }
103
+ end
104
+
93
105
  public_class_method def self.authors
94
106
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
95
107
  end
@@ -147,6 +159,11 @@ module PWN
147
159
  script: 'optional - Array of gdb/MI commands'
148
160
  )
149
161
 
162
+ # Probe yama ptrace_scope before gdb/frida attach.
163
+ #{self}.ptrace_preflight(
164
+ target: 'optional - pid or binary path being attached'
165
+ )
166
+
150
167
  # Print the AUTHOR(S) string for this module.
151
168
  #{self}.authors
152
169
  "
@@ -40,6 +40,19 @@ module PWN
40
40
  row
41
41
  end
42
42
 
43
+ public_class_method def self.watch(opts = {})
44
+ row = load_job(opts)
45
+ pattern = opts[:pattern].to_s
46
+ raise 'ERROR: pattern is required' if pattern.empty?
47
+
48
+ log = row[:log].to_s
49
+ return { id: row[:id], hits: [] } unless File.file?(log)
50
+
51
+ rx = Regexp.new(pattern)
52
+ hits = File.readlines(log).grep(rx).last(20)
53
+ { id: row[:id], hits: hits.map(&:chomp), pattern: pattern }
54
+ end
55
+
43
56
  public_class_method def self.status(opts = {})
44
57
  row = load_job(opts)
45
58
  alive = begin
@@ -144,6 +157,12 @@ module PWN
144
157
  # Run stop and return its result
145
158
  #{self}.stop
146
159
 
160
+ # Tail a job log for a regex (AFL new-crash lines).
161
+ #{self}.watch(
162
+ id: 'required - job id from #start',
163
+ pattern: 'required - regex to match in the job log'
164
+ )
165
+
147
166
  # Print the AUTHOR(S) string for this module.
148
167
  #{self}.authors
149
168
  "
@@ -494,7 +494,12 @@ module PWN
494
494
  puts '[*] TAB menus: leading `/` = commands (/cron /skills /sessions …); `/` later = host paths; otherwise Ruby completion (same as the pwn REPL).'
495
495
  puts "[*] tmux + terminator users: Ensure ~/.tmux.conf has 'set -s extended-keys on' and 'set -g xterm-keys on', then restart tmux. Use TERM=xterm-256color."
496
496
  tag = pi.config.pwn_ai_session_id.to_s.empty? ? '<SESSION_ID>' : pi.config.pwn_ai_session_id
497
- puts "\n\n\npwn-ai debug ON → ~/.pwn/logs/pwn-ai-DEBUG-#{tag}-R<REQUEST_NUMBER>.log"
497
+
498
+ dbg_lvl = ''
499
+ dbg_lvl = 'trace' if pi.config.pwn_ai_trace
500
+ dbg_lvl = 'debug' if pi.config.pwn_ai_debug && !pi.config.pwn_ai_trace
501
+
502
+ puts "\n\n\npwn-ai #{dbg_lvl} ON → ~/.pwn/logs/pwn-ai-DEBUG-#{tag}-R<REQUEST_NUMBER>.log" unless dbg_lvl.empty?
498
503
  end
499
504
  end
500
505
 
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.721'
4
+ VERSION = '0.5.722'
5
5
  end
@@ -216,4 +216,14 @@ describe PWN::AI::Agent::Mistakes do
216
216
  expect(classes).to eq(rows.map { |r| r['class'] })
217
217
  expect(classes.uniq.length).to eq(3)
218
218
  end
219
+
220
+ it 'gives pull-access-denied an auth family hint, never ls-the-parent' do
221
+ stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(Dir.mktmpdir, 'mistakes.json'))
222
+ described_class.reset
223
+ described_class.record(tool: 'shell', error: 'pull access denied for library/foo')
224
+ hint = described_class.correction_hint(tool: 'shell', error: 'pull access denied for library/foo')
225
+ expect(hint).to match(/credential|registry/i)
226
+ expect(hint).not_to match(%r{ls/test -e the parent})
227
+ expect(described_class.family(error: 'pull access denied')).to eq('auth_denied')
228
+ end
219
229
  end
@@ -438,4 +438,16 @@ describe 'PWN::AI::Agent::Reward vs TUI plan' do
438
438
  expect(v[:verifier_verdict]).to eq(:pass)
439
439
  expect(v[:judge_score]).to be < 0.6
440
440
  end
441
+
442
+ it 'tags tool-backed heuristics so they are not haircut' do
443
+ allow(PWN::AI::Agent::Reward).to receive(:llm_judge).and_return(nil)
444
+ v = PWN::AI::Agent::Reward.judge(
445
+ request: 'write /tmp/x and verify',
446
+ final: 'PASS file exists',
447
+ trace: ['{"success":true,"result":{"exit":0}}'],
448
+ commit: false
449
+ )
450
+ expect(v[:heuristic_class].to_s).to eq('toolbacked')
451
+ expect(v[:score]).to be >= 0.6
452
+ end
441
453
  end
@@ -22,4 +22,13 @@ describe PWN::Plugins::ArtifactRegistry do
22
22
  expect(rows.first[:kind]).to eq('loot')
23
23
  end
24
24
  end
25
+
26
+ it 'greps an artifact by regex' do
27
+ Dir.mktmpdir do |dir|
28
+ src = File.join(dir, 'dump.txt')
29
+ File.write(src, "nop\ncall system\nret\n")
30
+ hits = described_class.read_page(path: src, grep: 'call.*system')
31
+ expect(hits[:matches].first[:text]).to include('call system')
32
+ end
33
+ end
25
34
  end
@@ -280,9 +280,11 @@
280
280
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.compact! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.compact!`: "}]}
281
281
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.compress_exemplar Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.compress_exemplar`: "}]}
282
282
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.compress_finetune_trace Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.compress_finetune_trace`: "}]}
283
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.consistency_check Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.consistency_check`: "}]}
283
284
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.consolidate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.consolidate`: Supported Method Parameters\n\nremoved = PWN::AI::Agent::Learning.consolidate(\n\nmax_entries: 'optional - hard cap on PWN::Memory size (default MAX_MEMORY_ENTRIES)'\n\n)\n\nDeduplicates near-identical lesson values and prunes the oldest entries once the cap is exceeded so the injected MEMORY block stays high-signal.\n"}]}
284
285
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.discount_success_rate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.discount_success_rate`: "}]}
285
286
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.display_task Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.display_task`: "}]}
287
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.disputed_save Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.disputed_save`: "}]}
286
288
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.distill_skill Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.distill_skill`: Supported Method Parameters\n\nskill = PWN::AI::Agent::Learning.distill_skill(\n\nname: 'required - snake_case name for the new skill',\nsession_id: 'optional - PWN::Sessions id to mine (uses its transcript)',\ncontent: 'optional - explicit markdown body; overrides transcript mining',\nreferences: 'optional - Array of reference URLs / CWE / CVE / ATT&CK ids'\n\n)\n"}]}
287
289
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.exemplars_for Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.exemplars_for`: Supported Method Parameters\n\nmsgs = PWN::AI::Agent::Learning.exemplars_for(\n\nrequest: 'required - current user request',\nlimit: 'optional - max exemplar traces to return (default 1)',\nmax_msgs: 'optional - cap on messages per exemplar (default 6)'\n\n)\n\nRetrieval-augmented BEHAVIOUR: keyword-matches request against prior successful outcomes in learning.jsonl, loads the matching session, and compresses its (user, tool, assistant) trace into a short few-shot exemplar Loop.run splices between system and user. Local models are dramatically better with 1 concrete example than with 25 abstract lessons.\n"}]}
288
290
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.export_finetune Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.export_finetune`: "}]}
@@ -419,6 +421,7 @@
419
421
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.run Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.run`: Supported Method Parameters\n\nfinal = PWN::AI::Agent::Loop.run(\n\nrequest: 'required - what the human typed',\nsession_id: 'optional - PWN::Sessions id (transcript is appended to it)',\nenabled_toolsets: 'optional - subset of Registry.toolsets, or nil for all',\non_tool: 'optional - ->(name, args, result) callback for live UI',\nsystem_role_content: 'optional - override default system prompt (built from session_id if not provided)'\n\n)\n"}]}
420
422
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.session_chat_history Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.session_chat_history`: "}]}
421
423
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.session_files Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.session_files`: "}]}
424
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.session_tool_budget Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.session_tool_budget`: "}]}
422
425
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.should_auto_introspect? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.should_auto_introspect?`: "}]}
423
426
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.spill_tool_body Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.spill_tool_body`: "}]}
424
427
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.spill_tool_history! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.spill_tool_history!`: "}]}
@@ -467,6 +470,7 @@
467
470
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.save Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.save`: Supported Method Parameters\n\nPWN::AI::Agent::Metrics.save(\n\nmetrics: 'required - Hash returned by .load / mutated in place'\n\n)\n"}]}
468
471
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.scale_prediction Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.scale_prediction`: "}]}
469
472
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.scoreboard Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.scoreboard`: "}]}
473
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.snapshot Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.snapshot`: "}]}
470
474
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.summary Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.summary`: Supported Method Parameters\n\nrows = PWN::AI::Agent::Metrics.summary(\n\nlimit: 'optional - cap number of tools returned (default 25)',\nengine: 'optional - only that engine\\'s sub-bucket (falls back to global when absent)'\n\n)\n"}]}
471
475
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.thompson Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.thompson`: Supported Method Parameters\n\np = PWN::AI::Agent::Metrics.thompson(name: ‘shell’)\n\nC1 — Thompson sample from Beta(ok+1, fail+1). Naturally balances exploit/explore; used by Registry.rank as the tie-breaker.\n"}]}
472
476
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.to_context Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.to_context`: Supported Method Parameters\n\nctx = PWN::AI::Agent::Metrics.to_context(\n\nlimit: 'optional - cap number of tools included (default 8)',\nengine: 'optional - restrict to one engine\\'s telemetry'\n\n)\n"}]}
@@ -481,6 +485,7 @@
481
485
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.error_class Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.error_class`: "}]}
482
486
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.extinguish! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.extinguish!`: "}]}
483
487
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.extinguish_parked! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.extinguish_parked!`: "}]}
488
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.family Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.family`: "}]}
484
489
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.find Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.find`: Supported Method Parameters\n\nentry = PWN::AI::Agent::Mistakes.find(\n\nsignature: 'optional - exact signature to fetch',\ntool: 'optional - with error:, compute signature and fetch',\nerror: 'optional - raw error text (used with tool:)'\n\n)\n"}]}
485
490
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.for_tool Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.for_tool`: Supported Method Parameters\n\nrows = PWN::AI::Agent::Mistakes.for_tool(\n\ntool: 'required - tool name',\nunresolved_only: 'optional - default false'\n\n)\n"}]}
486
491
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.help`: "}]}
@@ -488,6 +493,7 @@
488
493
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.lean! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.lean!`: Supported Method Parameters\n\nresult = PWN::AI::Agent::Mistakes.lean!(\n\ndry_run: 'optional - Boolean (default false)',\nmax_resolved_kept: 'optional - cap on resolved-with-fix records',\nresolved_min_age_days: 'optional - age before resolved count=1 may drop'\n\n)\n\nCompact text fields on every record. Never drops unresolved, regressed, or high-count repeaters. Aged resolved-once fixes may drop after Memory already holds mistake_fix_<sig>.\n"}]}
489
494
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.load Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.load`: Supported Method Parameters\n\nstore = PWN::AI::Agent::Mistakes.load\n"}]}
490
495
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.normalize_error Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.normalize_error`: "}]}
496
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.note_hint_outcome Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.note_hint_outcome`: "}]}
491
497
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.operator_inbox Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.operator_inbox`: "}]}
492
498
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.park Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.park`: "}]}
493
499
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Mistakes.previous_assistant Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Mistakes.previous_assistant`: "}]}
@@ -2068,6 +2074,7 @@
2068
2074
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.flat Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.flat`: "}]}
2069
2075
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.fmt_writes Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.fmt_writes`: "}]}
2070
2076
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.fmtstr Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.fmtstr`: "}]}
2077
+ {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.from_crash Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.from_crash`: "}]}
2071
2078
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.gadgets Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.gadgets`: "}]}
2072
2079
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.help Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.help`: "}]}
2073
2080
  {"messages":[{"role":"user","content":"PWN::Plugins::ExploitDev.io Usage"},{"role":"assistant","content":"`PWN::Plugins::ExploitDev.io`: "}]}
@@ -2121,6 +2128,7 @@
2121
2128
  {"messages":[{"role":"user","content":"PWN::Plugins::GDB.debug_session Usage"},{"role":"assistant","content":"`PWN::Plugins::GDB.debug_session`: "}]}
2122
2129
  {"messages":[{"role":"user","content":"PWN::Plugins::GDB.help Usage"},{"role":"assistant","content":"`PWN::Plugins::GDB.help`: "}]}
2123
2130
  {"messages":[{"role":"user","content":"PWN::Plugins::GDB.mitigations Usage"},{"role":"assistant","content":"`PWN::Plugins::GDB.mitigations`: "}]}
2131
+ {"messages":[{"role":"user","content":"PWN::Plugins::GDB.ptrace_preflight Usage"},{"role":"assistant","content":"`PWN::Plugins::GDB.ptrace_preflight`: "}]}
2124
2132
  {"messages":[{"role":"user","content":"PWN::Plugins::GDB.registers Usage"},{"role":"assistant","content":"`PWN::Plugins::GDB.registers`: "}]}
2125
2133
  {"messages":[{"role":"user","content":"PWN::Plugins::GDB.required_bins Usage"},{"role":"assistant","content":"`PWN::Plugins::GDB.required_bins`: "}]}
2126
2134
  {"messages":[{"role":"user","content":"PWN::Plugins::GDB.run_to_crash Usage"},{"role":"assistant","content":"`PWN::Plugins::GDB.run_to_crash`: "}]}
@@ -2293,6 +2301,7 @@
2293
2301
  {"messages":[{"role":"user","content":"PWN::Plugins::Jobs.status Usage"},{"role":"assistant","content":"`PWN::Plugins::Jobs.status`: "}]}
2294
2302
  {"messages":[{"role":"user","content":"PWN::Plugins::Jobs.stop Usage"},{"role":"assistant","content":"`PWN::Plugins::Jobs.stop`: "}]}
2295
2303
  {"messages":[{"role":"user","content":"PWN::Plugins::Jobs.tail Usage"},{"role":"assistant","content":"`PWN::Plugins::Jobs.tail`: "}]}
2304
+ {"messages":[{"role":"user","content":"PWN::Plugins::Jobs.watch Usage"},{"role":"assistant","content":"`PWN::Plugins::Jobs.watch`: "}]}
2296
2305
  {"messages":[{"role":"user","content":"PWN::Plugins::K8s.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::K8s.authors`: "}]}
2297
2306
  {"messages":[{"role":"user","content":"PWN::Plugins::K8s.docker_socket? Usage"},{"role":"assistant","content":"`PWN::Plugins::K8s.docker_socket?`: "}]}
2298
2307
  {"messages":[{"role":"user","content":"PWN::Plugins::K8s.help Usage"},{"role":"assistant","content":"`PWN::Plugins::K8s.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.721
4
+ version: 0.5.722
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.