pwn 0.5.720 → 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 +4 -4
- data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/loop/SKILL.md +4 -2
- data/etc/default_skills/pwn/ai/agent/metrics/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/mistakes/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/registry/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/gdb/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
- data/lib/pwn/ai/agent/dispatch.rb +15 -5
- data/lib/pwn/ai/agent/learning.rb +52 -1
- data/lib/pwn/ai/agent/loop.rb +74 -26
- data/lib/pwn/ai/agent/metrics.rb +53 -1
- data/lib/pwn/ai/agent/mistakes.rb +67 -1
- data/lib/pwn/ai/agent/registry.rb +28 -0
- data/lib/pwn/ai/agent/reward.rb +15 -10
- data/lib/pwn/ai/agent/tool_guard.rb +54 -9
- data/lib/pwn/ai/agent/tools/artifacts.rb +8 -3
- data/lib/pwn/ai/agent/turn_finalizer.rb +20 -1
- data/lib/pwn/plugins/artifact_registry.rb +54 -8
- data/lib/pwn/plugins/binary_parser.rb +4 -1
- data/lib/pwn/plugins/exploit_dev.rb +48 -0
- data/lib/pwn/plugins/findings.rb +9 -1
- data/lib/pwn/plugins/gdb.rb +17 -0
- data/lib/pwn/plugins/ghidra.rb +20 -2
- data/lib/pwn/plugins/jobs.rb +19 -0
- data/lib/pwn/plugins/preflight_checker.rb +23 -0
- data/lib/pwn/plugins/process_tube.rb +31 -1
- data/lib/pwn/plugins/repl.rb +6 -1
- data/lib/pwn/sessions.rb +4 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +9 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +10 -0
- data/spec/lib/pwn/ai/agent/reward_spec.rb +13 -1
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +9 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +9 -0
- data/spec/lib/pwn/plugins/findings_spec.rb +4 -4
- data/third_party/pwn_rdoc.jsonl +20 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31f54d1cd6a7bfbc4cef3bcc989650930b1b31bbf2bbc47654b20d6f20158775
|
|
4
|
+
data.tar.gz: 50bfda31eecd4b99d17324a8c4c548f14b8df10561c183f27bc9e7157c8f88bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a43118f36da1f9712dbfb908b5f0c9d4650d3bfde63239240d7c9cefcc50d29d78468e14e9cd9bf44f9fdba5df971b48659b348619dc79a7e17667028930025
|
|
7
|
+
data.tar.gz: 867d72ec34f85caaf18fa5abeb38b466d5663c1361becf17cb4af87115dd0d45411527b0f5616dd54874c241544416fb0b9427f16476248ed38a864b3951daef
|
|
@@ -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.
|
|
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?(:
|
|
64
|
+
`PWN::AI::Agent::Loop.respond_to?(:evidence_satisfied)` after the
|
|
63
65
|
module is loaded. Read the source for parameter names.
|
|
@@ -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`
|
|
@@ -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?`
|
|
@@ -52,7 +52,8 @@ module PWN
|
|
|
52
52
|
args = expand_vault_args(args: args)
|
|
53
53
|
end
|
|
54
54
|
if defined?(Engagement)
|
|
55
|
-
denied =
|
|
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)
|
|
@@ -71,6 +72,15 @@ module PWN
|
|
|
71
72
|
error: 'refused: memory_remember/skills_update text copied from last tool output'
|
|
72
73
|
)
|
|
73
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
|
|
74
84
|
result = entry.handler.call(args)
|
|
75
85
|
result = ToolGuard.quarantine_output(text: result) if defined?(ToolGuard) && result.is_a?(String) && ToolGuard.respond_to?(:quarantine_output)
|
|
76
86
|
note_taint(text: result)
|
|
@@ -343,8 +353,8 @@ module PWN
|
|
|
343
353
|
return :store if STORE_TOOLS.include?(name)
|
|
344
354
|
|
|
345
355
|
blob = argv_blob(args: opts[:args])
|
|
346
|
-
return :browse if blob.match?(BROWSE_ARGV_RX)
|
|
347
356
|
return :write if blob.match?(WRITE_ARGV_RX)
|
|
357
|
+
return :browse if blob.match?(BROWSE_ARGV_RX)
|
|
348
358
|
return :eval if name == 'pwn_eval'
|
|
349
359
|
|
|
350
360
|
:read
|
|
@@ -381,7 +391,7 @@ module PWN
|
|
|
381
391
|
|
|
382
392
|
private_class_method def self.note_taint(opts = {})
|
|
383
393
|
text = opts[:text].to_s
|
|
384
|
-
grams = text.scan(/.{
|
|
394
|
+
grams = text.scan(/.{24,}/).first(20)
|
|
385
395
|
store = Thread.current[:pwn_taint] ||= []
|
|
386
396
|
grams.each { |g| store << g[0, 64] }
|
|
387
397
|
store.shift while store.length > 200
|
|
@@ -393,10 +403,10 @@ module PWN
|
|
|
393
403
|
return false if mode == 'off'
|
|
394
404
|
|
|
395
405
|
blob = opts[:args].inspect
|
|
396
|
-
return false if blob.length <
|
|
406
|
+
return false if blob.length < 24
|
|
397
407
|
return false if opts[:args].is_a?(Hash) && (opts[:args][:taint_ack] == true || opts[:args]['taint_ack'] == true)
|
|
398
408
|
|
|
399
|
-
hit = Array(Thread.current[:pwn_taint]).any? { |g| g.length >=
|
|
409
|
+
hit = Array(Thread.current[:pwn_taint]).any? { |g| g.length >= 24 && blob.include?(g) }
|
|
400
410
|
return false unless hit
|
|
401
411
|
return false unless blob.match?(/curl |bash -c|sh -c|\|\s*sh\b/i)
|
|
402
412
|
|
|
@@ -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)',
|
|
@@ -196,11 +219,21 @@ module PWN
|
|
|
196
219
|
jmean = total.positive? ? weighted_judge_mean(rows: rows) : nil
|
|
197
220
|
distrust = 0.0
|
|
198
221
|
distrust = Reward.proxy_distrust.to_f.clamp(0.0, 1.0) if defined?(Reward) && Reward.respond_to?(:proxy_distrust)
|
|
222
|
+
orm = rows.select { |r| r[:judge_source].to_s != 'heuristic' && r[:source].to_s != 'heuristic' }
|
|
223
|
+
heur = rows.select { |r| r[:judge_source].to_s == 'heuristic' || r[:source].to_s == 'heuristic' }
|
|
224
|
+
orm_n = orm.length
|
|
225
|
+
heur_n = heur.length
|
|
226
|
+
orm_ok = orm.count { |r| r[:success] == true }
|
|
227
|
+
heur_ok = heur.count { |r| r[:success] == true }
|
|
199
228
|
{
|
|
200
229
|
total_outcomes: total,
|
|
201
230
|
successes: ok,
|
|
202
231
|
failures: total - ok,
|
|
203
232
|
success_rate: raw,
|
|
233
|
+
success_rate_orm: orm_n.positive? ? (orm_ok.to_f / orm_n).round(3) : 0.0,
|
|
234
|
+
success_rate_heur: heur_n.positive? ? (heur_ok.to_f / heur_n).round(3) : 0.0,
|
|
235
|
+
orm_n: orm_n,
|
|
236
|
+
heur_n: heur_n,
|
|
204
237
|
adjusted_success_rate: discount_success_rate(proxy: raw, judge: jmean, distrust: distrust),
|
|
205
238
|
proxy_distrust: distrust,
|
|
206
239
|
skills_known: skills,
|
|
@@ -227,6 +260,12 @@ module PWN
|
|
|
227
260
|
fails = prefer_primary_tasks(rows: outcomes(limit: 200, success: false))
|
|
228
261
|
fails = fails.reject { |r| r[:status].to_s == 'conflicted' }
|
|
229
262
|
fails = fails.reject { |r| r[:verifier_verdict].to_s == 'pass' }
|
|
263
|
+
fails = fails.reject { |r| r[:details].to_s.match?(/\bPASS\b/) && r[:success] == true }
|
|
264
|
+
fails = fails.select do |r|
|
|
265
|
+
v = r[:verdict].to_s
|
|
266
|
+
v == 'wrong' || v == 'refused' || (r[:score].to_f < 0.3 && !r[:details].to_s.match?(/\bPASS\b/)) || r[:success] == false
|
|
267
|
+
end
|
|
268
|
+
fails = fails.reject { |r| r[:success] == true }
|
|
230
269
|
# Do not mirror the same ids under both headings — that doubled the
|
|
231
270
|
# failure signal and made RECENT OUTCOMES == RECENT FAILURES when the
|
|
232
271
|
# last N attempts all failed (the injected block looked "stuck").
|
|
@@ -260,7 +299,7 @@ module PWN
|
|
|
260
299
|
d = s[:proxy_distrust].to_f
|
|
261
300
|
rate = d > 0.05 ? s[:adjusted_success_rate] : s[:success_rate]
|
|
262
301
|
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)"
|
|
302
|
+
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
303
|
out = "#{hdr}\n#{rows.map(&fmt).join("\n")}\n"
|
|
265
304
|
out += "RECENT FAILURES (learn from these — do not repeat)\n#{fails.map(&fmt).join("\n")}\n" unless fails.empty?
|
|
266
305
|
"#{out}\n"
|
|
@@ -2065,6 +2104,18 @@ module PWN
|
|
|
2065
2104
|
include_demoted: 'optional - include demoted lessons (defaults to false)'
|
|
2066
2105
|
)
|
|
2067
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
|
+
|
|
2068
2119
|
# Print the AUTHOR(S) string for this module.
|
|
2069
2120
|
#{self}.authors
|
|
2070
2121
|
"
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -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
|
-
|
|
623
|
+
if idx
|
|
624
|
+
tail = effects[(idx + 1)..] || []
|
|
625
|
+
return true if tail.intersect?(%i[read eval])
|
|
626
|
+
end
|
|
611
627
|
|
|
612
|
-
|
|
613
|
-
|
|
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
|
|
642
|
+
|
|
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],
|
|
@@ -1145,6 +1176,16 @@ module PWN
|
|
|
1145
1176
|
false
|
|
1146
1177
|
end
|
|
1147
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
|
+
|
|
1148
1189
|
# E1 — did the environment change under this tool? If Metrics CUSUM
|
|
1149
1190
|
# tripped for it in the last hour AND Extrospection.drift shows a
|
|
1150
1191
|
# toolchain/net/repo change, blame the WORLD not the AGENT.
|
|
@@ -1678,6 +1719,7 @@ module PWN
|
|
|
1678
1719
|
}
|
|
1679
1720
|
# Ollama + abliterated / weak chat-templates often ignore tools: and
|
|
1680
1721
|
# answer in prose (or print shell(...) as text). Force native
|
|
1722
|
+
|
|
1681
1723
|
# tool_calls until at least one tool result is already in history;
|
|
1682
1724
|
# after that, auto so the model can emit a real final answer.
|
|
1683
1725
|
# Respect explicit PWN::Env[:ai][:ollama][:tool_choice] override.
|
|
@@ -1705,10 +1747,6 @@ module PWN
|
|
|
1705
1747
|
end
|
|
1706
1748
|
end
|
|
1707
1749
|
|
|
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
1750
|
private_class_method def self.session_chat_history(opts = {})
|
|
1713
1751
|
return [] unless defined?(PWN::Sessions) && PWN::Sessions.respond_to?(:to_llm_messages)
|
|
1714
1752
|
|
|
@@ -1831,15 +1869,15 @@ module PWN
|
|
|
1831
1869
|
|
|
1832
1870
|
private_class_method def self.spill_tool_body(opts = {})
|
|
1833
1871
|
text = opts[:text].to_s
|
|
1834
|
-
digest = Digest::SHA256.hexdigest(text)
|
|
1835
|
-
dir =
|
|
1872
|
+
digest = Digest::SHA256.hexdigest(text)
|
|
1873
|
+
dir = File.join(Dir.home, '.pwn', 'artifacts', 'transcripts', (Thread.current[:pwn_session_id] || 'default').to_s)
|
|
1836
1874
|
FileUtils.mkdir_p(dir)
|
|
1837
|
-
path = File.join(dir, "#{digest}.txt")
|
|
1875
|
+
path = File.join(dir, "#{digest[0, 12]}.txt")
|
|
1838
1876
|
File.binwrite(path, text) unless File.file?(path)
|
|
1839
1877
|
head = text.byteslice(0, 2_048).to_s
|
|
1840
1878
|
tail = text.bytesize > 3_072 ? text.byteslice(-1_024, 1_024).to_s : ''
|
|
1841
1879
|
mid = tail.empty? ? '' : "\n...\n#{tail}"
|
|
1842
|
-
"#{head}#{mid}\n[compacted path=#{path} sha256=#{digest} bytes=#{text.bytesize}]"
|
|
1880
|
+
"#{head}#{mid}\n[compacted path=#{path} sha256=#{digest} bytes=#{text.bytesize} ref=#{path}]"
|
|
1843
1881
|
rescue StandardError
|
|
1844
1882
|
"[compacted bytes=#{opts[:text].to_s.bytesize}]"
|
|
1845
1883
|
end
|
|
@@ -3002,17 +3040,21 @@ module PWN
|
|
|
3002
3040
|
text: text
|
|
3003
3041
|
)
|
|
3004
3042
|
turn_fails['unsatisfied'] += 1
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3043
|
+
if turn_fails['unsatisfied'] >= 2 && evidence_satisfied?(request: request, messages: messages, text: text)
|
|
3044
|
+
debug_progress(msg: 'nag cap: evidence_satisfied after 2 bounces')
|
|
3045
|
+
else
|
|
3046
|
+
unmet = completion_unmet(request: request, messages: messages)
|
|
3047
|
+
warn "[pwn-ai/loop] original request not evidenced on iter=#{i} unmet=#{unmet.join(',')}; continuing"
|
|
3048
|
+
debug_progress(msg: "bounce unsatisfied unmet=#{unmet.join(',')} snippet=#{debug_snippet(text: text)}")
|
|
3049
|
+
messages << {
|
|
3050
|
+
role: 'user',
|
|
3051
|
+
content: "[pwn-ai] The original request is not evidenced yet. unmet=#{unmet.join(',')} " \
|
|
3052
|
+
'Keep calling CORE_TOOLS (shell, pwn_eval) until that request is ' \
|
|
3053
|
+
'done or a tool returned failure evidence. pwn-ai does not decide ' \
|
|
3054
|
+
'authorization. Do not declare completion from a listing or a refusal.'
|
|
3055
|
+
}
|
|
3056
|
+
next
|
|
3057
|
+
end
|
|
3016
3058
|
end
|
|
3017
3059
|
debug_progress(msg: "final accepted chars=#{text.to_s.length}")
|
|
3018
3060
|
quiet_debug_tui!(reason: 'final')
|
|
@@ -3094,7 +3136,7 @@ module PWN
|
|
|
3094
3136
|
append_session(
|
|
3095
3137
|
session_id: session_id,
|
|
3096
3138
|
role: 'tool',
|
|
3097
|
-
content: "#{name} → #{result[0,
|
|
3139
|
+
content: "#{name} → #{result[0, session_tool_budget(name: name, result: result)]}"
|
|
3098
3140
|
)
|
|
3099
3141
|
end
|
|
3100
3142
|
|
|
@@ -3163,8 +3205,6 @@ module PWN
|
|
|
3163
3205
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
3164
3206
|
end
|
|
3165
3207
|
|
|
3166
|
-
# Display Usage for this Module
|
|
3167
|
-
|
|
3168
3208
|
public_class_method def self.help
|
|
3169
3209
|
puts "USAGE:
|
|
3170
3210
|
# Run debug on and return its result
|
|
@@ -3172,6 +3212,14 @@ module PWN
|
|
|
3172
3212
|
debug: 'optional - debug value consumed by #debug_on?'
|
|
3173
3213
|
)
|
|
3174
3214
|
|
|
3215
|
+
# True when write-then-readback evidence satisfies the original request.
|
|
3216
|
+
#{self}.evidence_satisfied?(
|
|
3217
|
+
messages: 'optional - Array of role/content hashes',
|
|
3218
|
+
trace: 'optional - alias for messages',
|
|
3219
|
+
text: 'optional - final answer text',
|
|
3220
|
+
request: 'optional - original request'
|
|
3221
|
+
)
|
|
3222
|
+
|
|
3175
3223
|
# True only when the ask needs a live host/file/browser effect. World-knowledge
|
|
3176
3224
|
#{self}.catalog_lookup?(
|
|
3177
3225
|
request: 'required - request value consumed by #catalog_lookup?'
|
data/lib/pwn/ai/agent/metrics.rb
CHANGED
|
@@ -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)',
|
|
@@ -133,6 +156,19 @@ module PWN
|
|
|
133
156
|
.sort_by { |r| [-r[:calls], -r[:success_rate]] }.first(limit)
|
|
134
157
|
end
|
|
135
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
|
+
|
|
136
172
|
# Supported Method Parameters::
|
|
137
173
|
# ctx = PWN::AI::Agent::Metrics.to_context(
|
|
138
174
|
# limit: 'optional - cap number of tools included (default 8)',
|
|
@@ -664,11 +700,22 @@ module PWN
|
|
|
664
700
|
#{self}.record(
|
|
665
701
|
name: 'required - tool name that was dispatched',
|
|
666
702
|
success: 'required - Boolean, did the handler complete without error',
|
|
667
|
-
duration: 'optional - Float seconds the dispatch took',
|
|
703
|
+
duration: 'optional - Float seconds the dispatch took (wall time)',
|
|
668
704
|
error: 'optional - String error message when success is false',
|
|
669
705
|
engine: 'optional - Symbol/String AI engine that chose this tool (segments telemetry)'
|
|
670
706
|
)
|
|
671
707
|
|
|
708
|
+
# Append one JSONL telemetry row under ~/.pwn/logs/tool_metrics.jsonl.
|
|
709
|
+
#{self}.append_jsonl(
|
|
710
|
+
name: 'required - tool name',
|
|
711
|
+
success: 'optional - Boolean outcome',
|
|
712
|
+
duration: 'optional - Float seconds the dispatch took (wall time)',
|
|
713
|
+
error: 'optional - error string',
|
|
714
|
+
engine: 'optional - engine name',
|
|
715
|
+
timeout: 'optional - timeout used',
|
|
716
|
+
bytes_out: 'optional - output byte count'
|
|
717
|
+
)
|
|
718
|
+
|
|
672
719
|
# Run summary and return its result
|
|
673
720
|
#{self}.summary(
|
|
674
721
|
limit: 'optional - cap number of tools returned (default 25)',
|
|
@@ -780,6 +827,11 @@ module PWN
|
|
|
780
827
|
# Run reset and return its result
|
|
781
828
|
#{self}.reset
|
|
782
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
|
+
|
|
783
835
|
# Print the AUTHOR(S) string for this module.
|
|
784
836
|
#{self}.authors
|
|
785
837
|
"
|