pwn 0.5.721 → 0.5.723
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/bin/pwn_setup +5 -5
- data/documentation/AI-Integration.md +34 -1
- data/documentation/Policy-Benchmark.md +151 -0
- data/documentation/Reinforcement-Learning.md +27 -1
- data/etc/default_skills/pwn/ai/agent/engagement/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/metrics/SKILL.md +4 -0
- data/etc/default_skills/pwn/ai/agent/mistakes/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/policy/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/reward/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +6 -0
- data/etc/default_skills/pwn/ai/agent/tools/capabilities/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/context/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/context/SKILL.md +50 -0
- data/etc/default_skills/pwn/ai/http_retry/SKILL.md +7 -0
- data/etc/default_skills/pwn/ai/http_retry/references/urls.md +4 -0
- data/etc/default_skills/pwn/ai/open_ai/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/open_ai/references/urls.md +1 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/gdb/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/gdbmi/SKILL.md +55 -0
- data/etc/default_skills/pwn/plugins/ghidra_headless/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +6 -0
- data/etc/default_skills/pwn/plugins/packet/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/radare2/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +3 -0
- data/etc/default_skills/pwn/reports/engagement/SKILL.md +3 -2
- data/lib/pwn/ai/agent/curriculum.rb +37 -45
- data/lib/pwn/ai/agent/dispatch.rb +9 -0
- data/lib/pwn/ai/agent/engagement.rb +59 -0
- data/lib/pwn/ai/agent/learning.rb +111 -44
- data/lib/pwn/ai/agent/loop.rb +50 -8
- data/lib/pwn/ai/agent/metrics.rb +70 -2
- data/lib/pwn/ai/agent/mistakes.rb +67 -1
- data/lib/pwn/ai/agent/policy.rb +142 -16
- data/lib/pwn/ai/agent/registry.rb +1 -1
- data/lib/pwn/ai/agent/reward.rb +153 -53
- data/lib/pwn/ai/agent/swarm.rb +235 -35
- data/lib/pwn/ai/agent/tool_guard.rb +13 -1
- data/lib/pwn/ai/agent/tools/artifacts.rb +50 -3
- data/lib/pwn/ai/agent/tools/capabilities.rb +19 -0
- data/lib/pwn/ai/agent/tools/context.rb +38 -0
- data/lib/pwn/ai/agent/tools/finding_record.rb +18 -0
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +10 -1
- data/lib/pwn/ai/agent/tools/job_run.rb +32 -0
- data/lib/pwn/ai/agent/tools/learning.rb +5 -6
- data/lib/pwn/ai/agent/tools/metrics.rb +16 -0
- data/lib/pwn/ai/agent/tools/pty_session.rb +4 -4
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +6 -5
- data/lib/pwn/ai/agent/tools/shell.rb +10 -1
- data/lib/pwn/ai/agent/tools/skills.rb +30 -0
- data/lib/pwn/ai/agent/tools/swarm.rb +8 -2
- data/lib/pwn/ai/context.rb +193 -0
- data/lib/pwn/ai/http_retry.rb +53 -7
- data/lib/pwn/ai/open_ai.rb +302 -45
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/migrate.rb +10 -1
- data/lib/pwn/plugins/artifact_registry.rb +40 -17
- data/lib/pwn/plugins/binary_parser.rb +4 -1
- data/lib/pwn/plugins/exploit_dev.rb +15 -0
- data/lib/pwn/plugins/findings.rb +48 -8
- data/lib/pwn/plugins/gdb.rb +17 -0
- data/lib/pwn/plugins/gdbmi.rb +128 -0
- data/lib/pwn/plugins/ghidra_headless.rb +104 -0
- data/lib/pwn/plugins/jobs.rb +72 -0
- data/lib/pwn/plugins/packet.rb +51 -0
- data/lib/pwn/plugins/preflight_checker.rb +29 -0
- data/lib/pwn/plugins/process_tube.rb +24 -7
- data/lib/pwn/plugins/radare2.rb +14 -2
- data/lib/pwn/plugins/repl.rb +6 -1
- data/lib/pwn/plugins/transparent_browser.rb +64 -0
- data/lib/pwn/plugins.rb +2 -0
- data/lib/pwn/reports/engagement.rb +19 -0
- data/lib/pwn/sessions.rb +3 -1
- data/lib/pwn/version.rb +1 -1
- data/scripts/benchmark_policy.rb +276 -0
- data/spec/documentation/installation_md_spec.rb +18 -4
- data/spec/integration/reinforced_feedback_loop_spec.rb +20 -13
- data/spec/lib/pwn/ai/agent/curriculum_spec.rb +267 -0
- data/spec/lib/pwn/ai/agent/engagement_spec.rb +12 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +81 -3
- data/spec/lib/pwn/ai/agent/loop_spec.rb +51 -0
- data/spec/lib/pwn/ai/agent/metrics_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +10 -0
- data/spec/lib/pwn/ai/agent/policy_spec.rb +103 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +62 -0
- data/spec/lib/pwn/ai/agent/reward_spec.rb +175 -12
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +121 -1
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +6 -0
- data/spec/lib/pwn/ai/agent/tools/capabilities_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/context_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +25 -0
- data/spec/lib/pwn/ai/context_spec.rb +48 -0
- data/spec/lib/pwn/ai/http_retry_spec.rb +27 -0
- data/spec/lib/pwn/ai/open_ai_oauth_transport_spec.rb +245 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +150 -0
- data/spec/lib/pwn/migrate_spec.rb +24 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +19 -0
- data/spec/lib/pwn/plugins/findings_spec.rb +2 -0
- data/spec/lib/pwn/plugins/gdbmi_spec.rb +17 -0
- data/spec/lib/pwn/plugins/ghidra_headless_spec.rb +17 -0
- data/third_party/pwn_rdoc.jsonl +86 -2
- metadata +24 -5
|
@@ -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
|
|
@@ -102,18 +103,20 @@ module PWN
|
|
|
102
103
|
|
|
103
104
|
tags = Array(opts[:tags]).map(&:to_s)
|
|
104
105
|
details = opts[:details].to_s[0, OUTCOME_DETAILS_MAX]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
decision = if opts[:outcome].is_a?(Hash)
|
|
107
|
+
Reward.resolve_outcome(outcome: opts[:outcome])
|
|
108
|
+
elsif opts.key?(:score) && success != 'soft'
|
|
109
|
+
Reward.resolve_outcome(outcome: { score: opts[:score], source: opts[:judge_source] || :manual, confidence: opts[:confidence] })
|
|
110
|
+
end
|
|
111
|
+
if decision || opts.key?(:score)
|
|
112
|
+
score = decision ? decision[:score] : opts[:score].to_f
|
|
113
|
+
want = (decision ? decision[:verdict] : verdict_for_score(score: score)).to_s
|
|
111
114
|
tags = (tags - %w[solved partial wrong unknown]) << want
|
|
112
115
|
details = details.sub(
|
|
113
116
|
/\A(solved|partial|wrong|unknown)\(\d+(?:\.\d+)?\)/i,
|
|
114
|
-
"#{want}(#{format('%.2f', score)})"
|
|
117
|
+
"#{want}(#{score.nil? ? 'unknown' : format('%.2f', score)})"
|
|
115
118
|
)
|
|
116
|
-
success =
|
|
119
|
+
success = decision[:success] if decision
|
|
117
120
|
end
|
|
118
121
|
|
|
119
122
|
entry = {
|
|
@@ -125,18 +128,27 @@ module PWN
|
|
|
125
128
|
tags: tags,
|
|
126
129
|
timestamp: Time.now.utc.iso8601
|
|
127
130
|
}
|
|
128
|
-
entry[:score] =
|
|
131
|
+
entry[:score] = score if decision || opts.key?(:score)
|
|
132
|
+
if decision
|
|
133
|
+
%i[verdict confidence training_score decision_version verification verifier_verdict grounded critic_pass judge_score].each do |key|
|
|
134
|
+
entry[key] = decision[key] if decision.key?(key)
|
|
135
|
+
end
|
|
136
|
+
entry[:status] = 'unverified' if decision[:training_score].nil?
|
|
137
|
+
end
|
|
129
138
|
src = opts[:judge_source].to_s
|
|
139
|
+
src = decision[:source].to_s if decision && decision[:source]
|
|
130
140
|
entry[:judge_source] = src unless src.empty?
|
|
131
|
-
vv = (opts[:verifier_verdict] || opts['verifier_verdict']).to_s
|
|
141
|
+
vv = (decision ? decision[:verifier_verdict] : opts[:verifier_verdict] || opts['verifier_verdict']).to_s
|
|
132
142
|
entry[:verifier_verdict] = vv unless vv.empty?
|
|
133
143
|
vc = (opts[:verdict_class] || opts['verdict_class']).to_s
|
|
134
144
|
entry[:verdict_class] = vc unless vc.empty?
|
|
135
145
|
entry[:remediation_hint] = opts[:remediation_hint].to_s unless opts[:remediation_hint].to_s.empty?
|
|
136
|
-
if
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
entry[:status] = 'conflicted' if opts[:verifier_verdict].to_s == 'pass' && !entry[:verification] && opts[:score].to_f < 0.6
|
|
147
|
+
check = consistency_check(details: details, success: success, rationale: opts[:rationale])
|
|
148
|
+
if check == :disputed
|
|
149
|
+
entry[:status] = 'disputed'
|
|
150
|
+
disputed_save(entry: entry)
|
|
151
|
+
return entry
|
|
140
152
|
end
|
|
141
153
|
FileUtils.mkdir_p(File.dirname(LEARNING_FILE))
|
|
142
154
|
File.open(LEARNING_FILE, 'a') { |f| f.puts(JSON.generate(entry)) }
|
|
@@ -147,18 +159,33 @@ module PWN
|
|
|
147
159
|
# are promoted into PWN::Memory[:lesson] so PromptBuilder recall
|
|
148
160
|
# survives across sessions. Without this, the agent re-learns
|
|
149
161
|
# "run rubocop after every patch" every turn (empty memory.json).
|
|
150
|
-
promote_process_lesson(entry: entry) if defined?(PWN::Memory) && entry[:status].to_s
|
|
151
|
-
if opts.key?(:score) && defined?(Curriculum) && Curriculum.respond_to?(:calibrate)
|
|
162
|
+
promote_process_lesson(entry: entry) if defined?(PWN::Memory) && !%w[conflicted unverified].include?(entry[:status].to_s)
|
|
163
|
+
if opts.key?(:score) && (!decision || !decision[:training_score].nil?) && defined?(Curriculum) && Curriculum.respond_to?(:calibrate)
|
|
152
164
|
pred = opts[:predicted]
|
|
153
165
|
pred = Thread.current[:pwn_plan_predicted] if pred.nil?
|
|
154
166
|
pred = opts[:confidence] if pred.nil?
|
|
155
167
|
eng = opts[:engine]
|
|
156
168
|
eng = (PWN::Env.dig(:ai, :active) if defined?(PWN::Env)) if eng.to_s.empty?
|
|
157
|
-
Curriculum.calibrate(predicted: pred, actual: opts[:score], engine: eng)
|
|
169
|
+
Curriculum.calibrate(predicted: pred, actual: decision ? decision[:training_score] : opts[:score], engine: eng)
|
|
158
170
|
end
|
|
159
171
|
entry
|
|
160
172
|
end
|
|
161
173
|
|
|
174
|
+
public_class_method def self.consistency_check(opts = {})
|
|
175
|
+
outcome = opts[:outcome]
|
|
176
|
+
return :ok unless outcome.is_a?(Hash)
|
|
177
|
+
return :disputed if Reward.resolve_outcome(outcome: outcome)[:success] != opts[:success]
|
|
178
|
+
|
|
179
|
+
:ok
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
public_class_method def self.disputed_save(opts = {})
|
|
183
|
+
entry = opts[:entry] || {}
|
|
184
|
+
FileUtils.mkdir_p(File.dirname(DISPUTED_FILE))
|
|
185
|
+
File.open(DISPUTED_FILE, 'a') { |f| f.puts(JSON.generate(entry)) }
|
|
186
|
+
entry
|
|
187
|
+
end
|
|
188
|
+
|
|
162
189
|
# Supported Method Parameters::
|
|
163
190
|
# rows = PWN::AI::Agent::Learning.outcomes(
|
|
164
191
|
# limit: 'optional - max entries returned newest-first (default 50)',
|
|
@@ -178,6 +205,7 @@ module PWN
|
|
|
178
205
|
nil
|
|
179
206
|
end
|
|
180
207
|
rows.compact!
|
|
208
|
+
rows.reject! { |r| r[:decision_version] && r[:training_score].nil? } unless want_ok.nil?
|
|
181
209
|
rows.select! { |r| want_ok == true ? r[:success] == true : r[:success] == want_ok } unless want_ok.nil?
|
|
182
210
|
rows.select! { |r| Array(r[:tags]).any? { |t| t.to_s.downcase.include?(tag) } } unless tag.empty?
|
|
183
211
|
rows.reverse.first(limit)
|
|
@@ -189,11 +217,13 @@ module PWN
|
|
|
189
217
|
public_class_method def self.stats
|
|
190
218
|
rows = outcomes(limit: 10_000)
|
|
191
219
|
total = rows.length
|
|
220
|
+
rows = rows.reject { |r| (r[:decision_version] && r[:training_score].nil?) || r[:success].nil? }
|
|
221
|
+
evaluated = rows.length
|
|
192
222
|
ok = rows.count { |r| r[:success] == true }
|
|
193
223
|
skills = defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) ? PWN::Skills.keys.length : 0
|
|
194
224
|
mem = defined?(PWN::Memory) ? PWN::Memory.load.keys.length : 0
|
|
195
|
-
raw =
|
|
196
|
-
jmean =
|
|
225
|
+
raw = evaluated.positive? ? (ok.to_f / evaluated).round(3) : 0.0
|
|
226
|
+
jmean = evaluated.positive? ? weighted_judge_mean(rows: rows) : nil
|
|
197
227
|
distrust = 0.0
|
|
198
228
|
distrust = Reward.proxy_distrust.to_f.clamp(0.0, 1.0) if defined?(Reward) && Reward.respond_to?(:proxy_distrust)
|
|
199
229
|
orm = rows.select { |r| r[:judge_source].to_s != 'heuristic' && r[:source].to_s != 'heuristic' }
|
|
@@ -204,8 +234,9 @@ module PWN
|
|
|
204
234
|
heur_ok = heur.count { |r| r[:success] == true }
|
|
205
235
|
{
|
|
206
236
|
total_outcomes: total,
|
|
237
|
+
unknown_outcomes: total - evaluated,
|
|
207
238
|
successes: ok,
|
|
208
|
-
failures:
|
|
239
|
+
failures: rows.count { |r| r[:success] == false },
|
|
209
240
|
success_rate: raw,
|
|
210
241
|
success_rate_orm: orm_n.positive? ? (orm_ok.to_f / orm_n).round(3) : 0.0,
|
|
211
242
|
success_rate_heur: heur_n.positive? ? (heur_ok.to_f / heur_n).round(3) : 0.0,
|
|
@@ -251,17 +282,22 @@ module PWN
|
|
|
251
282
|
return '' if rows.empty? && fails.empty?
|
|
252
283
|
|
|
253
284
|
fmt = lambda do |r|
|
|
285
|
+
unknown = r[:status].to_s == 'unverified' || (r[:decision_version] && r[:training_score].nil?)
|
|
254
286
|
flag = case r[:success]
|
|
255
287
|
when true then '✓'
|
|
256
288
|
when 'soft', :soft then '∼'
|
|
257
289
|
else '✗'
|
|
258
290
|
end
|
|
259
291
|
score = r.key?(:score) ? format('%.2f', r[:score].to_f) : '-'
|
|
292
|
+
if unknown
|
|
293
|
+
flag = '?'
|
|
294
|
+
score = 'unknown'
|
|
295
|
+
end
|
|
260
296
|
task = display_task(task: r[:task])
|
|
261
297
|
line = " #{flag} [#{score}] #{task} (#{r[:timestamp]})"
|
|
262
298
|
# Surface a one-line cause crumb so the agent can actually learn
|
|
263
299
|
# from failures instead of only seeing that they failed.
|
|
264
|
-
if r[:success] != true
|
|
300
|
+
if r[:success] != true && !unknown
|
|
265
301
|
if r[:verdict_class].to_s == ''
|
|
266
302
|
crumb = cause_crumb(details: r[:details])
|
|
267
303
|
line += "\n cause: #{crumb}" unless crumb.empty?
|
|
@@ -609,14 +645,10 @@ module PWN
|
|
|
609
645
|
|
|
610
646
|
# R1 judge — always attempt (heuristic is cheap; LLM gated inside)
|
|
611
647
|
stages_run << :judge
|
|
612
|
-
|
|
613
|
-
v
|
|
614
|
-
v
|
|
615
|
-
|
|
616
|
-
# producing learning.jsonl rows tagged "solved" with success=false
|
|
617
|
-
# (116+ rows). Always resync verdict/success from the final score.
|
|
618
|
-
v[:verdict] = verdict_for_score(score: v[:score])
|
|
619
|
-
v[:success] = v[:score].to_f >= 0.6
|
|
648
|
+
critic_pass = crit[:verdict] == :flaw ? false : nil
|
|
649
|
+
v = Reward.judge(request: opts[:request], final: opts[:final], session_id: session_id, proxy_ok: proxy_ok, predicted: opts[:predicted], critic_pass: critic_pass) if defined?(Reward)
|
|
650
|
+
v ||= { score: nil, source: :error, verdict: :unknown, success: nil }
|
|
651
|
+
v = Reward.resolve_outcome(outcome: v, critic_pass: critic_pass)
|
|
620
652
|
ok = v[:success]
|
|
621
653
|
|
|
622
654
|
# W1 pending user_correction pair
|
|
@@ -664,20 +696,23 @@ module PWN
|
|
|
664
696
|
task: task_txt,
|
|
665
697
|
success: ok,
|
|
666
698
|
score: v[:score],
|
|
699
|
+
outcome: v,
|
|
667
700
|
details: "#{v[:verdict]}(#{v[:score].to_f.round(2)}) #{v[:rationale]} | #{opts[:final].to_s[0, 200]}",
|
|
668
701
|
session_id: session_id,
|
|
669
702
|
tags: outcome_tags,
|
|
670
703
|
judge_source: v[:source]
|
|
671
704
|
)
|
|
672
705
|
|
|
673
|
-
|
|
674
|
-
|
|
706
|
+
unless v[:training_score].nil?
|
|
707
|
+
stages_run << :fold_judge
|
|
708
|
+
fold_judge_into_metrics(session_id: session_id, score: v[:training_score], confidence: v[:confidence])
|
|
709
|
+
end
|
|
675
710
|
# R5 — close the live MDP episode with the ORM terminal reward.
|
|
676
711
|
if defined?(PWN::AI::Agent::Policy) && Policy.respond_to?(:finish)
|
|
677
712
|
stages_run << :policy
|
|
678
713
|
Policy.finish(
|
|
679
714
|
session_id: session_id,
|
|
680
|
-
score: v[:
|
|
715
|
+
score: v[:training_score],
|
|
681
716
|
confidence: v[:confidence],
|
|
682
717
|
verdict: v[:verdict],
|
|
683
718
|
proxy_ok: ok,
|
|
@@ -687,7 +722,7 @@ module PWN
|
|
|
687
722
|
end
|
|
688
723
|
|
|
689
724
|
# R2 PRM — skip under hard cap (expensive LLM); keep under soft if heuristic path
|
|
690
|
-
if over_hard.call || !defined?(Reward) || v[:
|
|
725
|
+
if over_hard.call || !defined?(Reward) || v[:training_score].nil? || !ok
|
|
691
726
|
stages_skipped << :prm
|
|
692
727
|
else
|
|
693
728
|
stages_run << :prm
|
|
@@ -695,7 +730,7 @@ module PWN
|
|
|
695
730
|
end
|
|
696
731
|
|
|
697
732
|
# C3 HER — only on failure; skip hard
|
|
698
|
-
if !ok && defined?(Curriculum) && !over_hard.call
|
|
733
|
+
if !ok && !v[:training_score].nil? && defined?(Curriculum) && !over_hard.call
|
|
699
734
|
stages_run << :hindsight
|
|
700
735
|
Curriculum.hindsight(request: opts[:request], final: opts[:final], session_id: session_id)
|
|
701
736
|
else
|
|
@@ -711,7 +746,7 @@ module PWN
|
|
|
711
746
|
# M4.1 — also reflect when the request/final is a process SOP
|
|
712
747
|
# (code hygiene) even if judge score < 0.6, so rubocop/rake
|
|
713
748
|
# lessons still land in PWN::Memory.
|
|
714
|
-
process_sop = process_sop_text?(text: "#{opts[:request]} #{opts[:final]}")
|
|
749
|
+
process_sop = !v[:training_score].nil? && process_sop_text?(text: "#{opts[:request]} #{opts[:final]}")
|
|
715
750
|
if (ok || process_sop) && !over_soft.call
|
|
716
751
|
stages_run << :reflect
|
|
717
752
|
reflect(session_id: session_id)
|
|
@@ -1105,6 +1140,8 @@ module PWN
|
|
|
1105
1140
|
lines = File.readlines(LEARNING_FILE)
|
|
1106
1141
|
out = lines.map do |l|
|
|
1107
1142
|
r = JSON.parse(l, symbolize_names: true)
|
|
1143
|
+
next l if r[:decision_version]
|
|
1144
|
+
|
|
1108
1145
|
score = r.key?(:score) ? r[:score].to_f : nil
|
|
1109
1146
|
next l if score.nil?
|
|
1110
1147
|
|
|
@@ -1874,14 +1911,31 @@ module PWN
|
|
|
1874
1911
|
|
|
1875
1912
|
n = 0
|
|
1876
1913
|
rows.each do |r|
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1914
|
+
next if r[:session_id].to_s.empty?
|
|
1915
|
+
|
|
1916
|
+
transcript = PWN::Sessions.load(session_id: r[:session_id])
|
|
1917
|
+
user_idx = transcript.rindex { |entry| entry[:role].to_s == 'user' }
|
|
1918
|
+
next unless user_idx
|
|
1919
|
+
|
|
1920
|
+
request = transcript[user_idx][:content].to_s
|
|
1921
|
+
final = transcript[(user_idx + 1)..].reverse.find { |entry| entry[:role].to_s == 'assistant' }
|
|
1922
|
+
next unless final && (request == r[:task].to_s || display_task(task: request) == r[:task].to_s)
|
|
1923
|
+
|
|
1924
|
+
outcome = Reward.judge(request: request, final: final[:content], session_id: r[:session_id], commit: false)
|
|
1925
|
+
fresh = note_outcome(
|
|
1926
|
+
task: request,
|
|
1927
|
+
session_id: r[:session_id],
|
|
1928
|
+
outcome: outcome,
|
|
1929
|
+
details: outcome[:rationale].to_s,
|
|
1930
|
+
tags: %w[requeue],
|
|
1931
|
+
judge_source: outcome[:source]
|
|
1884
1932
|
)
|
|
1933
|
+
updated = File.readlines(LEARNING_FILE).map do |line|
|
|
1934
|
+
row = JSON.parse(line, symbolize_names: true)
|
|
1935
|
+
row.merge!(status: 'rejudged', rescore_id: fresh[:id]) if row[:id] == r[:id]
|
|
1936
|
+
"#{JSON.generate(row)}\n"
|
|
1937
|
+
end
|
|
1938
|
+
File.write(LEARNING_FILE, updated.join)
|
|
1885
1939
|
n += 1
|
|
1886
1940
|
end
|
|
1887
1941
|
{ rescored: n, dry_run: false }
|
|
@@ -1915,14 +1969,16 @@ module PWN
|
|
|
1915
1969
|
task: 'required - short description of what was attempted',
|
|
1916
1970
|
success: 'required - Boolean, did the attempt achieve its goal',
|
|
1917
1971
|
details: 'optional - free-form notes / error / evidence',
|
|
1972
|
+
rationale: 'optional - judge explanation retained for consistency checks',
|
|
1918
1973
|
session_id: 'optional - PWN::Sessions id this outcome belongs to',
|
|
1919
1974
|
tags: 'optional - Array of String labels for later retrieval',
|
|
1920
1975
|
score: 'optional - score value consumed by #note_outcome',
|
|
1976
|
+
outcome: 'optional - canonical Reward outcome; preserves evidence, verdict and training eligibility',
|
|
1921
1977
|
judge_source: 'required - judge source value consumed by #note_outcome',
|
|
1922
1978
|
predicted: 'optional - predicted value consumed by #note_outcome',
|
|
1923
1979
|
confidence: 'optional - confidence value consumed by #note_outcome',
|
|
1924
1980
|
engine: 'optional - engine value consumed by #note_outcome',
|
|
1925
|
-
verifier_verdict: 'optional -
|
|
1981
|
+
verifier_verdict: 'optional - legacy diagnostic flag; cannot prove completion',
|
|
1926
1982
|
verdict_class: 'optional - missing_artifact|wrong_path|unverified_claim|scope_miss|partial_coverage|style_only',
|
|
1927
1983
|
remediation_hint: 'optional - one-line fix hint'
|
|
1928
1984
|
)
|
|
@@ -1932,7 +1988,7 @@ module PWN
|
|
|
1932
1988
|
limit: 'optional - max entries (defaults to 50)'
|
|
1933
1989
|
)
|
|
1934
1990
|
|
|
1935
|
-
#
|
|
1991
|
+
# Rejudge conflicted outcomes from the original session; never boost scores blindly.
|
|
1936
1992
|
#{self}.requeue_conflicted(
|
|
1937
1993
|
dry_run: 'optional - true to count without writing'
|
|
1938
1994
|
)
|
|
@@ -2081,6 +2137,17 @@ module PWN
|
|
|
2081
2137
|
include_demoted: 'optional - include demoted lessons (defaults to false)'
|
|
2082
2138
|
)
|
|
2083
2139
|
|
|
2140
|
+
# Compare structured decisions, never infer verification from prose.
|
|
2141
|
+
#{self}.consistency_check(
|
|
2142
|
+
outcome: 'optional - canonical outcome to compare against success',
|
|
2143
|
+
success: 'required - boolean success flag'
|
|
2144
|
+
)
|
|
2145
|
+
|
|
2146
|
+
# Append a disputed outcome to ~/.pwn/learning/disputed.jsonl.
|
|
2147
|
+
#{self}.disputed_save(
|
|
2148
|
+
entry: 'required - Hash of the disputed learning row'
|
|
2149
|
+
)
|
|
2150
|
+
|
|
2084
2151
|
# Print the AUTHOR(S) string for this module.
|
|
2085
2152
|
#{self}.authors
|
|
2086
2153
|
"
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -171,7 +171,7 @@ module PWN
|
|
|
171
171
|
return unless debug_on?(opts)
|
|
172
172
|
return unless defined?(PWN::Plugins::Log)
|
|
173
173
|
|
|
174
|
-
if defined?(TurnFinalizer) && TurnFinalizer.user_path?
|
|
174
|
+
if opts[:nested] || (defined?(TurnFinalizer) && TurnFinalizer.user_path?)
|
|
175
175
|
debug_progress(msg: 'nested Loop.run skip_roll', debug: opts[:debug])
|
|
176
176
|
return
|
|
177
177
|
end
|
|
@@ -704,7 +704,7 @@ module PWN
|
|
|
704
704
|
request = opts[:request].to_s
|
|
705
705
|
unmet = []
|
|
706
706
|
files = Array(contract[:paths]) + Array(contract[:proofs])
|
|
707
|
-
files.each { |path| unmet << "
|
|
707
|
+
files.each { |path| unmet << "deliverable_missing:#{path}" if deliverable_missing?(path: path) }
|
|
708
708
|
trace_files = session_files(messages: opts[:messages])
|
|
709
709
|
unmet << 'issue_work_proofs' if contract[:issue_work] && Array(contract[:proofs]).empty? && files.empty? && trace_files.empty?
|
|
710
710
|
unmet << 'skills' if declared_skills_missing?(skills: contract[:skills], request: request)
|
|
@@ -713,6 +713,7 @@ module PWN
|
|
|
713
713
|
asked_tech = Array(contract[:techniques]).select { |tech| request.downcase.include?(tech.to_s.downcase) }
|
|
714
714
|
unmet << 'hosts' if evidence_tokens_missing?(tokens: asked_hosts, blob: blob)
|
|
715
715
|
unmet << 'techniques' if evidence_tokens_missing?(tokens: asked_tech, blob: blob)
|
|
716
|
+
Array(Swarm.honesty_unmet(swarm_id: Thread.current[:pwn_swarm_id])).each { |u| unmet << u } if defined?(Swarm)
|
|
716
717
|
unmet
|
|
717
718
|
rescue StandardError
|
|
718
719
|
[]
|
|
@@ -1063,6 +1064,9 @@ module PWN
|
|
|
1063
1064
|
end
|
|
1064
1065
|
|
|
1065
1066
|
private_class_method def self.active_engine
|
|
1067
|
+
tl = Thread.current[:pwn_swarm_engine].to_s
|
|
1068
|
+
return tl.downcase.to_sym unless tl.empty?
|
|
1069
|
+
|
|
1066
1070
|
e = (PWN::Env.dig(:ai, :active) if defined?(PWN::Env)).to_s.downcase.to_sym
|
|
1067
1071
|
e == :'' ? :openai : e
|
|
1068
1072
|
rescue StandardError
|
|
@@ -1117,9 +1121,16 @@ module PWN
|
|
|
1117
1121
|
Metrics.record(name: name, success: sem[:semantic_ok], duration: dur, error: sem[:err], engine: opts[:engine]) if defined?(Metrics)
|
|
1118
1122
|
# R5 — live MDP step. Hygiene reward only; terminal credit is judge.
|
|
1119
1123
|
if defined?(PWN::AI::Agent::Policy) && Policy.respond_to?(:observe_step)
|
|
1124
|
+
policy_args = begin
|
|
1125
|
+
opts[:args].is_a?(String) ? JSON.parse(opts[:args]) : opts[:args]
|
|
1126
|
+
rescue JSON::ParserError
|
|
1127
|
+
nil
|
|
1128
|
+
end
|
|
1120
1129
|
Policy.observe_step(
|
|
1121
1130
|
session_id: opts[:session_id],
|
|
1122
1131
|
action: name,
|
|
1132
|
+
args: policy_args,
|
|
1133
|
+
result_type: sem[:shape],
|
|
1123
1134
|
ok: sem[:semantic_ok],
|
|
1124
1135
|
duration: dur,
|
|
1125
1136
|
engine: opts[:engine],
|
|
@@ -1176,6 +1187,16 @@ module PWN
|
|
|
1176
1187
|
false
|
|
1177
1188
|
end
|
|
1178
1189
|
|
|
1190
|
+
private_class_method def self.session_tool_budget(opts = {})
|
|
1191
|
+
name = opts[:name].to_s
|
|
1192
|
+
result = opts[:result].to_s
|
|
1193
|
+
return 16_384 if name.match?(/decompile|binary_triage|artifact_read|exploitdev/)
|
|
1194
|
+
return 16_384 if name == 'pwn_eval' && result.match?(/r2 |objdump|xxd |pdf\b|afl-/)
|
|
1195
|
+
return 16_384 if name == 'shell' && result.match?(/objdump|xxd|radare|r2 |disassembl/)
|
|
1196
|
+
|
|
1197
|
+
1_024
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1179
1200
|
# E1 — did the environment change under this tool? If Metrics CUSUM
|
|
1180
1201
|
# tripped for it in the last hour AND Extrospection.drift shows a
|
|
1181
1202
|
# toolchain/net/repo change, blame the WORLD not the AGENT.
|
|
@@ -1384,7 +1405,9 @@ module PWN
|
|
|
1384
1405
|
hint = Swarm.ask(
|
|
1385
1406
|
name: persona.to_s,
|
|
1386
1407
|
request: "Local agent is stuck on: #{request}\nFailed attempts: #{summary}\n" \
|
|
1387
|
-
'Give a 3-line corrective hint (which tool, which args, why). Reply with the hint ONLY.'
|
|
1408
|
+
'Give a 3-line corrective hint (which tool, which args, why). Reply with the hint ONLY.',
|
|
1409
|
+
swarm_id: Thread.current[:pwn_swarm_id],
|
|
1410
|
+
text_only: true
|
|
1388
1411
|
)
|
|
1389
1412
|
reply = hint.is_a?(Hash) ? hint[:reply].to_s : hint.to_s
|
|
1390
1413
|
Mistakes.record(tool: 'escalation', error: "local stuck after #{turn_fails.values.sum} fails; frontier hint requested", session_id: opts[:session_id], source: :loop) if defined?(Mistakes)
|
|
@@ -1692,10 +1715,12 @@ module PWN
|
|
|
1692
1715
|
|
|
1693
1716
|
mod = Object.const_get(mod_name)
|
|
1694
1717
|
if mod.respond_to?(:chat_with_tools)
|
|
1695
|
-
# xAI
|
|
1718
|
+
# xAI rejects Hash function.arguments / Hash content (422 map→string).
|
|
1719
|
+
# OpenAI sanitizes inside its provider so native Responses reasoning
|
|
1720
|
+
# survives until the credential-specific transport is selected.
|
|
1696
1721
|
# Ollama / Open WebUI reject *string* function.arguments (HTTP 400
|
|
1697
1722
|
# "can't find closing '}' symbol") — opposite of OpenAI wire form.
|
|
1698
|
-
wire_msgs = if
|
|
1723
|
+
wire_msgs = if engine == :grok
|
|
1699
1724
|
openai_wire_messages(messages: messages)
|
|
1700
1725
|
elsif local_engine?(engine: engine)
|
|
1701
1726
|
ollama_wire_messages(messages: messages)
|
|
@@ -2717,7 +2742,7 @@ module PWN
|
|
|
2717
2742
|
loud_debug_tui!(debug: opts[:debug])
|
|
2718
2743
|
debug_progress(msg: "Loop.run start request=#{request[0, 240]}", debug: opts[:debug])
|
|
2719
2744
|
ToolGuard.reset_timeout_budget! if defined?(ToolGuard) && ToolGuard.respond_to?(:reset_timeout_budget!)
|
|
2720
|
-
nested = defined?(TurnFinalizer) && TurnFinalizer.user_path?
|
|
2745
|
+
nested = opts[:nested] == true || Thread.current[:pwn_swarm_depth].to_i.positive? || (defined?(TurnFinalizer) && TurnFinalizer.user_path?)
|
|
2721
2746
|
TurnFinalizer.enter_user_path! if defined?(TurnFinalizer)
|
|
2722
2747
|
Thread.current[:pwn_loop_nested] = nested
|
|
2723
2748
|
bound = operator_bound_refusal(from: opts[:from] || opts[:account])
|
|
@@ -2932,6 +2957,14 @@ module PWN
|
|
|
2932
2957
|
|
|
2933
2958
|
t0 = Time.now
|
|
2934
2959
|
begin
|
|
2960
|
+
# Observations update the policy's context during execution.
|
|
2961
|
+
# Refresh exposure for the next hop without widening its scope
|
|
2962
|
+
# or substituting a generated goal for the original request.
|
|
2963
|
+
if tools_called.positive?
|
|
2964
|
+
tools = Registry.definitions(enabled: opts[:enabled_toolsets], relevance: request, core_only: core_only, intent: intent)
|
|
2965
|
+
no_tools = Array(tools).empty?
|
|
2966
|
+
Thread.current[:pwn_loop_no_tools] = no_tools
|
|
2967
|
+
end
|
|
2935
2968
|
repair_tool_history!(messages: messages)
|
|
2936
2969
|
msg = call_engine(messages: messages, tools: tools, ts_state: ts_state)
|
|
2937
2970
|
rescue StandardError => e
|
|
@@ -3126,7 +3159,7 @@ module PWN
|
|
|
3126
3159
|
append_session(
|
|
3127
3160
|
session_id: session_id,
|
|
3128
3161
|
role: 'tool',
|
|
3129
|
-
content: "#{name} → #{result[0,
|
|
3162
|
+
content: "#{name} → #{result[0, session_tool_budget(name: name, result: result)]}"
|
|
3130
3163
|
)
|
|
3131
3164
|
end
|
|
3132
3165
|
|
|
@@ -3151,6 +3184,11 @@ module PWN
|
|
|
3151
3184
|
end
|
|
3152
3185
|
raise
|
|
3153
3186
|
rescue StandardError => e
|
|
3187
|
+
if defined?(PWN::AI::HttpRetry) && PWN::AI::HttpRetry.quota_exhausted?(error: e)
|
|
3188
|
+
msg = PWN::AI::HttpRetry.quota_message(error: e)
|
|
3189
|
+
debug_progress(msg: "engine quota: #{msg}")
|
|
3190
|
+
return msg
|
|
3191
|
+
end
|
|
3154
3192
|
if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.respond_to?(:note_exception!)
|
|
3155
3193
|
PWN::Plugins::Log.note_exception!(error: e, where: 'Loop.run', which_self: self)
|
|
3156
3194
|
else
|
|
@@ -3250,7 +3288,11 @@ module PWN
|
|
|
3250
3288
|
debug: 'optional - debug value consumed by #run',
|
|
3251
3289
|
from: 'optional - sender account or address to bind as operator',
|
|
3252
3290
|
account: 'optional - operator account id to bind',
|
|
3253
|
-
force_tools: 'optional - force tools value consumed by #run'
|
|
3291
|
+
force_tools: 'optional - force tools value consumed by #run',
|
|
3292
|
+
nested: 'optional - true for Swarm/child Loop.run (skip RN footer)',
|
|
3293
|
+
core_only: 'optional - restrict to CORE_TOOLS when true',
|
|
3294
|
+
trace: 'optional - enable TracePoint debug for this run',
|
|
3295
|
+
debug_tee: 'optional - IO to tee debug logs'
|
|
3254
3296
|
)
|
|
3255
3297
|
|
|
3256
3298
|
# Remaining time/token/mutation budget for the current loop.
|
data/lib/pwn/ai/agent/metrics.rb
CHANGED
|
@@ -156,6 +156,49 @@ 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
|
+
|
|
172
|
+
public_class_method def self.record_tokens(opts = {})
|
|
173
|
+
n = opts[:tokens].to_i
|
|
174
|
+
cost = opts[:cost].to_f
|
|
175
|
+
model = opts[:model].to_s
|
|
176
|
+
m = load
|
|
177
|
+
m[:usage] ||= { tokens: 0, cost: 0.0, calls: 0, by_model: {} }
|
|
178
|
+
m[:usage][:tokens] += n
|
|
179
|
+
m[:usage][:cost] += cost
|
|
180
|
+
m[:usage][:calls] += 1
|
|
181
|
+
m[:usage][:by_model][model] ||= { tokens: 0, cost: 0.0, calls: 0 }
|
|
182
|
+
m[:usage][:by_model][model][:tokens] += n
|
|
183
|
+
m[:usage][:by_model][model][:cost] += cost
|
|
184
|
+
m[:usage][:by_model][model][:calls] += 1
|
|
185
|
+
save(metrics: m)
|
|
186
|
+
m[:usage]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
public_class_method def self.usage(opts = {})
|
|
190
|
+
_sid = opts[:session_id]
|
|
191
|
+
load[:usage] || { tokens: 0, cost: 0.0, calls: 0 }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
public_class_method def self.routing(opts = {})
|
|
195
|
+
_n = opts[:n]
|
|
196
|
+
chain = (PWN::Env.dig(:ai, :routing) if defined?(PWN::Env))
|
|
197
|
+
Array(chain)
|
|
198
|
+
rescue StandardError
|
|
199
|
+
[]
|
|
200
|
+
end
|
|
201
|
+
|
|
159
202
|
# Supported Method Parameters::
|
|
160
203
|
# ctx = PWN::AI::Agent::Metrics.to_context(
|
|
161
204
|
# limit: 'optional - cap number of tools included (default 8)',
|
|
@@ -544,9 +587,12 @@ module PWN
|
|
|
544
587
|
end
|
|
545
588
|
task_ok = nil
|
|
546
589
|
if defined?(Learning) && Learning.respond_to?(:outcomes)
|
|
547
|
-
rec = Learning.outcomes(limit: 200)
|
|
590
|
+
rec = Learning.outcomes(limit: 200).reject do |r|
|
|
591
|
+
r[:success].nil? || r[:status].to_s == 'unverified' || r[:verdict].to_s == 'unknown' ||
|
|
592
|
+
(r[:decision_version] && r[:training_score].nil?)
|
|
593
|
+
end
|
|
548
594
|
if rec.any?
|
|
549
|
-
hits = rec.count { |r| r[:success] == true
|
|
595
|
+
hits = rec.count { |r| r[:success] == true }
|
|
550
596
|
task_ok = (hits.to_f / rec.length).round(3)
|
|
551
597
|
end
|
|
552
598
|
end
|
|
@@ -814,6 +860,28 @@ module PWN
|
|
|
814
860
|
# Run reset and return its result
|
|
815
861
|
#{self}.reset
|
|
816
862
|
|
|
863
|
+
# Snapshot success rates per judge source for the LEARNING block.
|
|
864
|
+
#{self}.snapshot(
|
|
865
|
+
day: 'optional - reserved day key for rotation'
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
# Record token/cost for a model call.
|
|
869
|
+
#{self}.record_tokens(
|
|
870
|
+
tokens: 'optional - integer token count',
|
|
871
|
+
cost: 'optional - Float USD cost',
|
|
872
|
+
model: 'optional - model id string'
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
# Return cumulative token/cost usage.
|
|
876
|
+
#{self}.usage(
|
|
877
|
+
session_id: 'optional - unused reserved session id'
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
# Return ai.routing fallback chain from pwn.yaml.
|
|
881
|
+
#{self}.routing(
|
|
882
|
+
n: 'optional - unused reserved index'
|
|
883
|
+
)
|
|
884
|
+
|
|
817
885
|
# Print the AUTHOR(S) string for this module.
|
|
818
886
|
#{self}.authors
|
|
819
887
|
"
|
|
@@ -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
|
-
|
|
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
|
"
|