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
data/lib/pwn/plugins/radare2.rb
CHANGED
|
@@ -100,6 +100,11 @@ module PWN
|
|
|
100
100
|
cmdj(opts.merge(cmd: "axtj #{addr}"))
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
public_class_method def self.xrefs_from(opts = {})
|
|
104
|
+
addr = opts[:addr].to_s
|
|
105
|
+
cmdj(opts.merge(cmd: "axfj #{addr}"))
|
|
106
|
+
end
|
|
107
|
+
|
|
103
108
|
# Supported Method Parameters::
|
|
104
109
|
# PWN::Plugins::Radare2.disasm(
|
|
105
110
|
# session: 'required - session id returned by #open',
|
|
@@ -108,7 +113,7 @@ module PWN
|
|
|
108
113
|
# )
|
|
109
114
|
public_class_method def self.disasm(opts = {})
|
|
110
115
|
addr = opts[:addr].to_s
|
|
111
|
-
n = (opts[:n] || 32).to_i
|
|
116
|
+
n = (opts[:n] || opts[:len] || 32).to_i
|
|
112
117
|
cmd(opts.merge(cmd: "pd #{n} @ #{addr}"))
|
|
113
118
|
end
|
|
114
119
|
|
|
@@ -198,11 +203,18 @@ module PWN
|
|
|
198
203
|
addr: 'required - address or flag to list xrefs to (e.g. main or 0x401000)'
|
|
199
204
|
)
|
|
200
205
|
|
|
206
|
+
# List xrefs from an address or flag (axfj JSON).
|
|
207
|
+
#{self}.xrefs_from(
|
|
208
|
+
session: 'required - session id returned by #open',
|
|
209
|
+
addr: 'required - address or flag to list xrefs from'
|
|
210
|
+
)
|
|
211
|
+
|
|
201
212
|
# Disassemble n instructions at addr (pd text).
|
|
202
213
|
#{self}.disasm(
|
|
203
214
|
session: 'required - session id returned by #open',
|
|
204
215
|
addr: 'required - address or flag to disassemble from',
|
|
205
|
-
n: 'optional - instruction count (defaults to 32)'
|
|
216
|
+
n: 'optional - instruction count (defaults to 32)',
|
|
217
|
+
len: 'optional - alias for n, number of instructions to disassemble'
|
|
206
218
|
)
|
|
207
219
|
|
|
208
220
|
# List strings in the binary (izj JSON).
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
|
@@ -1571,6 +1571,50 @@ module PWN
|
|
|
1571
1571
|
{ screenshot: shot, dom: html, har: har, dir: dir }
|
|
1572
1572
|
end
|
|
1573
1573
|
|
|
1574
|
+
public_class_method def self.intercept(opts = {})
|
|
1575
|
+
browser_obj = opts[:browser_obj]
|
|
1576
|
+
patterns = Array(opts[:patterns] || opts[:urls] || ['*'])
|
|
1577
|
+
@intercepted ||= []
|
|
1578
|
+
begin
|
|
1579
|
+
browser = browser_obj.is_a?(Hash) ? browser_obj[:browser] : browser_obj
|
|
1580
|
+
drv = browser.respond_to?(:driver) ? browser.driver : nil
|
|
1581
|
+
drv.devtools.fetch.enable(patterns: patterns.map { |p| { urlPattern: p.to_s } }) if drv.respond_to?(:devtools)
|
|
1582
|
+
rescue StandardError
|
|
1583
|
+
nil
|
|
1584
|
+
end
|
|
1585
|
+
{ enabled: true, patterns: patterns, hook: 'Fetch.enable', buffer: @intercepted }
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1588
|
+
public_class_method def self.har_export(opts = {})
|
|
1589
|
+
sid = (opts[:session_id] || 'default').to_s
|
|
1590
|
+
entries = Array(opts[:entries] || @intercepted)
|
|
1591
|
+
har = { log: { version: '1.2', creator: { name: 'PWN::Plugins::TransparentBrowser', version: '1' }, entries: entries } }
|
|
1592
|
+
stored = PWN::Plugins::ArtifactRegistry.put(bytes: JSON.generate(har), kind: 'har', session_id: sid, tags: ['http'])
|
|
1593
|
+
stored.merge(har: har)
|
|
1594
|
+
end
|
|
1595
|
+
|
|
1596
|
+
public_class_method def self.replay(opts = {})
|
|
1597
|
+
req = opts[:request] || opts[:req] || {}
|
|
1598
|
+
req = JSON.parse(req, symbolize_names: true) if req.is_a?(String) && req.strip.start_with?('{')
|
|
1599
|
+
mutations = opts[:mutations] || {}
|
|
1600
|
+
url = (mutations[:url] || req[:url] || req['url']).to_s
|
|
1601
|
+
method = (mutations[:method] || req[:method] || req['method'] || 'GET').to_s
|
|
1602
|
+
headers = (req[:headers] || req['headers'] || {}).merge(mutations[:headers] || {})
|
|
1603
|
+
body = mutations[:body] || req[:body] || req['body']
|
|
1604
|
+
raise 'ERROR: request url is required' if url.empty?
|
|
1605
|
+
|
|
1606
|
+
require 'net/http'
|
|
1607
|
+
uri = URI.parse(url)
|
|
1608
|
+
klass = Net::HTTP.const_get(method.capitalize)
|
|
1609
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
1610
|
+
http.use_ssl = uri.scheme == 'https'
|
|
1611
|
+
r = klass.new(uri.request_uri)
|
|
1612
|
+
headers.each { |k, v| r[k.to_s] = v.to_s }
|
|
1613
|
+
r.body = body.to_s if body
|
|
1614
|
+
resp = http.request(r)
|
|
1615
|
+
{ status: resp.code.to_i, headers: resp.to_hash, body: resp.body.to_s[0, 65_536], url: url, method: method }
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1574
1618
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
1575
1619
|
|
|
1576
1620
|
public_class_method def self.authors
|
|
@@ -1723,6 +1767,26 @@ module PWN
|
|
|
1723
1767
|
session_id: 'optional - artifacts session folder (defaults to default)'
|
|
1724
1768
|
)
|
|
1725
1769
|
|
|
1770
|
+
# Enable CDP Fetch interception (Burp-like hook without Burp).
|
|
1771
|
+
#{self}.intercept(
|
|
1772
|
+
browser_obj: 'optional - browser_obj returned from #open',
|
|
1773
|
+
patterns: 'optional - Array of URL patterns (defaults to *)',
|
|
1774
|
+
urls: 'optional - alias for patterns'
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
# Export intercepted traffic as HAR into the artifact store.
|
|
1778
|
+
#{self}.har_export(
|
|
1779
|
+
session_id: 'optional - pwn-ai session id',
|
|
1780
|
+
entries: 'optional - Array of HAR entries (defaults to intercepted buffer)'
|
|
1781
|
+
)
|
|
1782
|
+
|
|
1783
|
+
# Replay an HTTP request with optional mutations (Repeater-class).
|
|
1784
|
+
#{self}.replay(
|
|
1785
|
+
request: 'required - Hash or JSON of method/url/headers/body',
|
|
1786
|
+
req: 'optional - alias for request',
|
|
1787
|
+
mutations: 'optional - Hash of url/method/headers/body overlays'
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1726
1790
|
# Print the AUTHOR(S) string for this module.
|
|
1727
1791
|
#{self}.authors
|
|
1728
1792
|
"
|
data/lib/pwn/plugins.rb
CHANGED
|
@@ -41,7 +41,9 @@ module PWN
|
|
|
41
41
|
autoload :ExploitDev, 'pwn/plugins/exploit_dev'
|
|
42
42
|
autoload :Git, 'pwn/plugins/git'
|
|
43
43
|
autoload :GDB, 'pwn/plugins/gdb'
|
|
44
|
+
autoload :GDBMI, 'pwn/plugins/gdbmi'
|
|
44
45
|
autoload :Ghidra, 'pwn/plugins/ghidra'
|
|
46
|
+
autoload :GhidraHeadless, 'pwn/plugins/ghidra_headless'
|
|
45
47
|
autoload :Github, 'pwn/plugins/github'
|
|
46
48
|
autoload :GoogleWorkspace, 'pwn/plugins/google_workspace'
|
|
47
49
|
autoload :HackerOne, 'pwn/plugins/hacker_one'
|
|
@@ -6,6 +6,16 @@ module PWN
|
|
|
6
6
|
module Reports
|
|
7
7
|
# Compile engagement findings into a client-ready HTML/Markdown report.
|
|
8
8
|
module Engagement
|
|
9
|
+
public_class_method def self.render(opts = {})
|
|
10
|
+
out = generate(opts)
|
|
11
|
+
fmt = (opts[:format] || 'md').to_s
|
|
12
|
+
case fmt
|
|
13
|
+
when 'html' then { path: out[:html], format: fmt }
|
|
14
|
+
when 'json' then { path: out[:json], format: fmt }
|
|
15
|
+
else { path: out[:markdown], format: 'md', all: out }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
9
19
|
public_class_method def self.generate(opts = {})
|
|
10
20
|
name = (opts[:engagement] || opts[:name] || 'default').to_s
|
|
11
21
|
rows = if opts[:findings]
|
|
@@ -53,6 +63,15 @@ module PWN
|
|
|
53
63
|
report_name: 'optional - basename without extension'
|
|
54
64
|
)
|
|
55
65
|
|
|
66
|
+
# Render one format (md|html|json) for a session/engagement.
|
|
67
|
+
#{self}.render(
|
|
68
|
+
session_id: 'optional - pwn-ai session id',
|
|
69
|
+
format: 'optional - md, html, or json (defaults to md)',
|
|
70
|
+
engagement: 'optional - engagement name',
|
|
71
|
+
dir_path: 'optional - output directory',
|
|
72
|
+
report_name: 'optional - basename without extension'
|
|
73
|
+
)
|
|
74
|
+
|
|
56
75
|
# Print the AUTHOR(S) string for this module.
|
|
57
76
|
#{self}.authors
|
|
58
77
|
"
|
data/lib/pwn/sessions.rb
CHANGED
|
@@ -612,7 +612,9 @@ module PWN
|
|
|
612
612
|
text = text.gsub(%r{Bearer\s+[A-Za-z0-9._\-+/=]{12,}}i) { |m| redacted_token(kind: 'bearer', value: m) }
|
|
613
613
|
text = text.gsub(/-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----/m) { |m| redacted_token(kind: 'pem', value: m) }
|
|
614
614
|
text = text.gsub(/(password\s*[:=]\s*)\S+/i) { "#{Regexp.last_match(1)}#{redacted_token(kind: 'password', value: Regexp.last_match(0))}" }
|
|
615
|
-
text.gsub(/Set-Cookie:\s*[^\r\n]+/i) { |m| redacted_token(kind: 'cookie', value: m) }
|
|
615
|
+
text = text.gsub(/Set-Cookie:\s*[^\r\n]+/i) { |m| redacted_token(kind: 'cookie', value: m) }
|
|
616
|
+
text = PWN::Plugins::Vault.redact(text: text) if defined?(PWN::Plugins::Vault)
|
|
617
|
+
text
|
|
616
618
|
end
|
|
617
619
|
|
|
618
620
|
private_class_method def self.redact_disabled?
|
data/lib/pwn/version.rb
CHANGED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'digest'
|
|
7
|
+
require 'optparse'
|
|
8
|
+
|
|
9
|
+
# Standalone experiment, deliberately outside the production module tree.
|
|
10
|
+
module PolicyBenchmark
|
|
11
|
+
def self.score(path:, expected:)
|
|
12
|
+
!File.symlink?(path) && File.file?(path) && JSON.parse(File.read(path)) == expected
|
|
13
|
+
rescue JSON::ParserError, SystemCallError
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.self_check
|
|
18
|
+
Dir.mktmpdir('pwn-policy-check-', '/tmp') do |root|
|
|
19
|
+
path = File.join(root, 'result.json')
|
|
20
|
+
File.write(path, JSON.generate([2, 10]))
|
|
21
|
+
raise 'checker rejected a correct artifact' unless score(path: path, expected: [2, 10])
|
|
22
|
+
|
|
23
|
+
File.write(path, 'PASS: all checks passed; result.json was verified successfully.')
|
|
24
|
+
raise 'checker accepted convincing prose' if score(path: path, expected: [2, 10])
|
|
25
|
+
|
|
26
|
+
File.write(path, JSON.generate([10, 2]))
|
|
27
|
+
raise 'checker accepted wrong artifact' if score(path: path, expected: [2, 10])
|
|
28
|
+
|
|
29
|
+
File.unlink(path)
|
|
30
|
+
raise 'checker accepted missing artifact' if score(path: path, expected: [2, 10])
|
|
31
|
+
|
|
32
|
+
other = File.join(root, 'other.json')
|
|
33
|
+
File.write(other, JSON.generate([2, 10]))
|
|
34
|
+
File.symlink(other, path)
|
|
35
|
+
raise 'checker accepted a symlink instead of a new artifact' if score(path: path, expected: [2, 10])
|
|
36
|
+
end
|
|
37
|
+
report = run
|
|
38
|
+
raise 'benchmark did not run both learning arms' unless report[:arms]&.keys == %i[off on]
|
|
39
|
+
|
|
40
|
+
report[:arms].each do |mode, arm|
|
|
41
|
+
raise 'missing held-out tasks' unless arm[:evaluation][:tasks] == 8
|
|
42
|
+
raise 'training contaminated held-out data' unless arm[:disjoint_inputs]
|
|
43
|
+
raise 'evaluation mutated the learned policy' unless arm[:policy_frozen]
|
|
44
|
+
raise 'negative controls trusted PASS text' unless arm[:negative_controls].length == 16 && arm[:negative_controls].all? { |row| row[:false_success] && !row[:completed] }
|
|
45
|
+
raise 'positive controls failed' unless arm[:positive_controls]&.length == 8 && arm[:positive_controls].all? { |row| row[:completed] }
|
|
46
|
+
raise 'training has no real executed actions' unless arm[:training][:tool_calls] == 72
|
|
47
|
+
raise 'off arm learned' if mode == :off && arm[:policy_stats][:n_updates].positive?
|
|
48
|
+
raise 'on arm did not learn' if mode == :on && !arm[:policy_stats][:n_updates].positive?
|
|
49
|
+
raise 'provider usage was invented' unless arm[:evaluation][:llm_calls].zero? && arm[:evaluation][:monetary_cost].nil?
|
|
50
|
+
end
|
|
51
|
+
puts 'Self-checks passed'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.run
|
|
55
|
+
raise 'Run in a fresh Ruby process; do not load the live agent' if defined?(PWN)
|
|
56
|
+
|
|
57
|
+
original_env = ENV.to_h
|
|
58
|
+
started = clock
|
|
59
|
+
Dir.mktmpdir('pwn-policy-benchmark-', '/tmp') do |root|
|
|
60
|
+
# Set HOME BEFORE requiring code with Dir.home-based constants. Do not
|
|
61
|
+
# load pwn.rb, user config, tools, network libraries, or provider clients.
|
|
62
|
+
ENV.replace('HOME' => root, 'TMPDIR' => root, 'LANG' => 'C.UTF-8')
|
|
63
|
+
Object.const_set(:PWN, Module.new)
|
|
64
|
+
PWN.const_set(:Env, { ai: { agent: { policy: false } } })
|
|
65
|
+
require_relative '../lib/pwn/ai/agent/policy'
|
|
66
|
+
require_relative '../lib/pwn/ai/agent/registry'
|
|
67
|
+
policy = PWN::AI::Agent::Policy
|
|
68
|
+
raise 'policy persistence escaped temporary HOME' unless [policy::POLICY_FILE, policy::TRAJECTORY_FILE].all? { |path| path.start_with?("#{root}/.pwn/") }
|
|
69
|
+
|
|
70
|
+
register_actions
|
|
71
|
+
arms = %i[off on].to_h { |mode| [mode, run_arm(root: root, mode: mode)] }
|
|
72
|
+
{
|
|
73
|
+
benchmark: 'pwn-policy-controller-v1',
|
|
74
|
+
scope: 'Deterministic local controller benchmark; NOT proof of live LLM improvement.',
|
|
75
|
+
ruby: RUBY_VERSION,
|
|
76
|
+
source_sha256: %w[policy registry].to_h { |name| [name, Digest::SHA256.file(File.expand_path("../lib/pwn/ai/agent/#{name}.rb", __dir__)).hexdigest] },
|
|
77
|
+
harness_sha256: Digest::SHA256.file(__FILE__).hexdigest,
|
|
78
|
+
isolation: { temporary_home: root, environment_cleared: true, network_calls: 0, persistence_removed_on_exit: true },
|
|
79
|
+
evaluation_updates: false, attempt_budget: 3, training_rounds: 6,
|
|
80
|
+
elapsed_seconds: clock - started, arms: arms
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
ensure
|
|
84
|
+
ENV.replace(original_env) if original_env
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.clock
|
|
88
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.fixtures(split:)
|
|
92
|
+
# Literal answer keys are not generated by, or passed to, action handlers.
|
|
93
|
+
numbers = if split == :train
|
|
94
|
+
[[[-2, 12, 3], [-2, 3, 12]], [[-5, 20, 1], [-5, 1, 20]]]
|
|
95
|
+
else
|
|
96
|
+
[[[-10, 4, 22, 0], [-10, 0, 4, 22]], [[31, 2, -3, 2], [-3, 2, 2, 31]],
|
|
97
|
+
[[7, 100, -8, 11], [-8, 7, 11, 100]], [[9, -1, 80, 6], [-1, 6, 9, 80]]]
|
|
98
|
+
end
|
|
99
|
+
inventory = if split == :train
|
|
100
|
+
[[[[1, true], [2, false]], [1]], [[[3, false], [4, true]], [4]]]
|
|
101
|
+
else
|
|
102
|
+
[[[[11, true], [12, false], [13, true]], [11, 13]],
|
|
103
|
+
[[[21, false], [22, false]], []],
|
|
104
|
+
[[[31, false], [32, true], [33, false]], [32]],
|
|
105
|
+
[[[41, true], [42, true], [43, false]], [41, 42]]]
|
|
106
|
+
end
|
|
107
|
+
{ numbers: numbers, inventory: inventory }.flat_map do |family, pairs|
|
|
108
|
+
pairs.each_with_index.map do |(input, expected), index|
|
|
109
|
+
{ id: "#{split}-#{family}-#{index}", family: family, input: input, expected: expected }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.query(family:)
|
|
115
|
+
family == :numbers ? 'fix numeric sorting artifact' : 'scan active inventory artifact'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.register_actions
|
|
119
|
+
actions = {
|
|
120
|
+
numbers: { a_lexical_sort: ->(data) { data.sort_by(&:to_s) }, b_numeric_sort: lambda(&:sort), c_numbers_claim: nil },
|
|
121
|
+
inventory: { a_active_filter: ->(data) { data.select { |row| row[1] == true }.map(&:first) }, b_all_records: ->(data) { data.map(&:first) }, c_inventory_claim: nil },
|
|
122
|
+
controls: { control_wrong_artifact: ->(_data) { { status: 'PASS: fully verified' } } }
|
|
123
|
+
}
|
|
124
|
+
actions.each do |family, implementations|
|
|
125
|
+
implementations.each do |name, implementation|
|
|
126
|
+
PWN::AI::Agent::Registry.register(
|
|
127
|
+
name: name, toolset: family,
|
|
128
|
+
schema: { name: name.to_s, description: query(family: family), parameters: { type: 'object', properties: {} } },
|
|
129
|
+
handler: lambda do |args|
|
|
130
|
+
data = JSON.parse(File.read(args.fetch(:input)))
|
|
131
|
+
File.write(args.fetch(:output), JSON.generate(implementation.call(data))) if implementation
|
|
132
|
+
{ ok: true, text: 'PASS: completed successfully. All checks passed; the artifact is correct and verified.' }
|
|
133
|
+
end
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.execute(root:, task:, action:, tag:)
|
|
140
|
+
directory = File.join(root, tag)
|
|
141
|
+
FileUtils.mkdir_p(directory)
|
|
142
|
+
input = File.join(directory, 'input.json')
|
|
143
|
+
output = File.join(directory, 'result.json')
|
|
144
|
+
raise 'task workspace reused' if File.exist?(input) || File.exist?(output)
|
|
145
|
+
|
|
146
|
+
File.write(input, JSON.generate(task[:input]))
|
|
147
|
+
input_digest = Digest::SHA256.file(input).hexdigest
|
|
148
|
+
started = clock
|
|
149
|
+
result = action.handler.call(input: input, output: output)
|
|
150
|
+
duration = clock - started
|
|
151
|
+
completed = score(path: output, expected: task[:expected]) && Digest::SHA256.file(input).hexdigest == input_digest
|
|
152
|
+
{
|
|
153
|
+
task_id: task[:id], family: task[:family], action: action.name,
|
|
154
|
+
completed: completed, claimed_success: result[:ok] == true,
|
|
155
|
+
false_success: result[:ok] == true && !completed,
|
|
156
|
+
checker: completed ? 'exact JSON answer and unchanged input' : 'missing/wrong artifact or changed input',
|
|
157
|
+
returned_text: result[:text], elapsed_seconds: duration, tool_calls: 1,
|
|
158
|
+
input_sha256: input_digest, artifact_sha256: File.file?(output) ? Digest::SHA256.file(output).hexdigest : nil
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.fingerprint(root:)
|
|
163
|
+
Dir[File.join(root, '.pwn', '**', '*')].select { |path| File.file?(path) }.sort.to_h do |path|
|
|
164
|
+
[path.delete_prefix("#{root}/"), Digest::SHA256.file(path).hexdigest]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.run_arm(root:, mode:)
|
|
169
|
+
policy = PWN::AI::Agent::Policy
|
|
170
|
+
registry = PWN::AI::Agent::Registry
|
|
171
|
+
policy.reset
|
|
172
|
+
PWN::Env[:ai][:agent][:policy] = mode == :on
|
|
173
|
+
train = fixtures(split: :train)
|
|
174
|
+
training_started = clock
|
|
175
|
+
training_rows = []
|
|
176
|
+
# Balanced, fixed exploration schedule: no answer-based action selection.
|
|
177
|
+
6.times do |round|
|
|
178
|
+
train.each do |task|
|
|
179
|
+
registry.all.select { |entry| entry.toolset == task[:family].to_s }.each do |action|
|
|
180
|
+
tag = "#{mode}/train/#{round}/#{task[:id]}/#{action.name}"
|
|
181
|
+
policy.begin_episode(session_id: tag, request: query(family: task[:family]))
|
|
182
|
+
row = execute(root: root, task: task, action: action, tag: tag)
|
|
183
|
+
policy.observe_step(session_id: tag, action: action.name, ok: row[:completed], duration: row[:elapsed_seconds])
|
|
184
|
+
update = policy.finish(session_id: tag, score: row[:completed] ? 1.0 : 0.0, confidence: 1.0, verdict: row[:completed] ? 'solved' : 'wrong')
|
|
185
|
+
raise "Policy.finish failed: #{update.inspect}" if update.nil? || update[:error]
|
|
186
|
+
|
|
187
|
+
training_rows << row.merge(policy_update: update)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
training = summarize(rows: training_rows, elapsed: clock - training_started)
|
|
192
|
+
stats = policy.stats
|
|
193
|
+
before = fingerprint(root: root)
|
|
194
|
+
# Held-out data is materialized only AFTER all training finishes.
|
|
195
|
+
heldout = fixtures(split: :heldout)
|
|
196
|
+
disjoint = !train.map { |task| task[:input] }.intersect?(heldout.map { |task| task[:input] })
|
|
197
|
+
raise 'training/evaluation input overlap' unless disjoint
|
|
198
|
+
|
|
199
|
+
evaluation_started = clock
|
|
200
|
+
evaluation_rows = heldout.flat_map do |task|
|
|
201
|
+
attempts = []
|
|
202
|
+
3.times do |attempt|
|
|
203
|
+
pool = registry.all.select { |entry| entry.toolset == task[:family].to_s }
|
|
204
|
+
# No begin/observe/finish during evaluation; Registry queries the
|
|
205
|
+
# learned table with its ordinary public fallback state.
|
|
206
|
+
ranked = registry.rank(query: query(family: task[:family]), entries: pool, preference: [])
|
|
207
|
+
raise 'Registry returned no action' if ranked.empty?
|
|
208
|
+
|
|
209
|
+
row = execute(root: root, task: task, action: ranked.first, tag: "#{mode}/heldout/#{task[:id]}/#{attempt}")
|
|
210
|
+
attempts << row.merge(attempt: attempt + 1, ranking: ranked.map(&:name))
|
|
211
|
+
break if row[:completed]
|
|
212
|
+
end
|
|
213
|
+
attempts
|
|
214
|
+
end
|
|
215
|
+
evaluation = summarize(rows: evaluation_rows, elapsed: clock - evaluation_started)
|
|
216
|
+
controls = heldout.flat_map do |task|
|
|
217
|
+
names = task[:family] == :numbers ? %w[c_numbers_claim control_wrong_artifact] : %w[c_inventory_claim control_wrong_artifact]
|
|
218
|
+
names.map do |name|
|
|
219
|
+
execute(root: root, task: task, action: registry.lookup(name: name), tag: "#{mode}/controls/#{task[:id]}/#{name}")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
positive_controls = heldout.map do |task|
|
|
223
|
+
name = task[:family] == :numbers ? 'b_numeric_sort' : 'a_active_filter'
|
|
224
|
+
execute(root: root, task: task, action: registry.lookup(name: name), tag: "#{mode}/positive/#{task[:id]}")
|
|
225
|
+
end
|
|
226
|
+
frozen = before == fingerprint(root: root)
|
|
227
|
+
raise 'held-out evaluation changed persisted policy' unless frozen
|
|
228
|
+
raise 'a negative control defeated the independent scorer' unless controls.all? { |row| row[:false_success] && !row[:completed] }
|
|
229
|
+
raise 'a positive control failed the independent scorer' unless positive_controls.all? { |row| row[:completed] }
|
|
230
|
+
|
|
231
|
+
{ disjoint_inputs: disjoint, policy_frozen: frozen, policy_stats: stats,
|
|
232
|
+
training: training, evaluation: evaluation, negative_controls: controls, positive_controls: positive_controls,
|
|
233
|
+
fixtures: { training: train, heldout: heldout },
|
|
234
|
+
policy_sha256: before, train_ids: train.map { |task| task[:id] }, heldout_ids: heldout.map { |task| task[:id] } }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def self.summarize(rows:, elapsed:)
|
|
238
|
+
tasks = rows.group_by { |row| row[:task_id] }
|
|
239
|
+
completed = tasks.count { |_, attempts| attempts.any? { |row| row[:completed] } }
|
|
240
|
+
failures = Hash.new(0)
|
|
241
|
+
rows.each do |row|
|
|
242
|
+
signature = [row[:family], row[:action], row[:checker]]
|
|
243
|
+
row[:repeated_mistake] = !row[:completed] && failures[signature].positive?
|
|
244
|
+
failures[signature] += 1 unless row[:completed]
|
|
245
|
+
end
|
|
246
|
+
{
|
|
247
|
+
tasks: tasks.length, completed: completed, completion_rate: completed.to_f / tasks.length,
|
|
248
|
+
false_successes: rows.count { |row| row[:false_success] },
|
|
249
|
+
false_success_rate: rows.count { |row| row[:false_success] }.to_f / rows.length,
|
|
250
|
+
repeated_mistakes: rows.count { |row| row[:repeated_mistake] },
|
|
251
|
+
tool_calls: rows.sum { |row| row[:tool_calls] }, elapsed_seconds: elapsed,
|
|
252
|
+
llm_calls: 0, tokens: 0, monetary_cost: nil,
|
|
253
|
+
cost_note: 'No provider invoked; monetary cost not estimated. Local compute cost is not priced.',
|
|
254
|
+
rows: rows
|
|
255
|
+
}
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
if $PROGRAM_NAME == __FILE__
|
|
260
|
+
options = {}
|
|
261
|
+
parser = OptionParser.new do |opts|
|
|
262
|
+
opts.banner = 'Usage: ruby scripts/benchmark_policy.rb [--self-check] [--output /tmp/report.json]'
|
|
263
|
+
opts.on('--self-check', 'Run scorer and end-to-end assertions') { options[:self_check] = true }
|
|
264
|
+
opts.on('--output PATH', 'Write the complete measured JSON report') { |path| options[:output] = path }
|
|
265
|
+
end
|
|
266
|
+
parser.parse!
|
|
267
|
+
abort parser.to_s unless ARGV.empty?
|
|
268
|
+
if options[:self_check]
|
|
269
|
+
PolicyBenchmark.self_check
|
|
270
|
+
else
|
|
271
|
+
report = PolicyBenchmark.run
|
|
272
|
+
json = JSON.pretty_generate(report)
|
|
273
|
+
File.write(options[:output], "#{json}\n") if options[:output]
|
|
274
|
+
puts json
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
require 'stringio'
|
|
5
5
|
require 'timeout'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
require 'open3'
|
|
6
8
|
require 'pwn/setup'
|
|
7
9
|
|
|
8
10
|
# Guards documentation/Installation.md from silently rotting.
|
|
@@ -110,10 +112,22 @@ RSpec.describe 'documentation/Installation.md' do
|
|
|
110
112
|
end
|
|
111
113
|
|
|
112
114
|
it '`pwn setup` and `pwn_setup` produce identical --list-profiles output' do
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
Dir.mktmpdir('pwn-list-profiles') do |home|
|
|
116
|
+
config_dir = File.join(home, '.pwn')
|
|
117
|
+
FileUtils.mkdir_p(config_dir)
|
|
118
|
+
config_path = File.join(config_dir, 'pwn.yaml')
|
|
119
|
+
fixture = "# unconfigured fixture; listing profiles must not load this file\n"
|
|
120
|
+
File.write(config_path, fixture)
|
|
121
|
+
outputs = [[File.join(repo_root, 'bin/pwn'), 'setup'], [File.join(repo_root, 'bin/pwn_setup')]].map do |command|
|
|
122
|
+
stdout, stderr, status = Open3.capture3({ 'HOME' => home }, RbConfig.ruby, *command, '--list-profiles', stdin_data: '')
|
|
123
|
+
expect(status.success?).to be(true), stderr
|
|
124
|
+
expect(stderr).to be_empty
|
|
125
|
+
stdout
|
|
126
|
+
end
|
|
127
|
+
expect(outputs.first).to eq(outputs.last)
|
|
128
|
+
PWN::Setup::PROFILES.each_key { |k| expect(outputs.first).to include(k.to_s) }
|
|
129
|
+
expect(File.read(config_path)).to eq(fixture)
|
|
130
|
+
end
|
|
117
131
|
end
|
|
118
132
|
end
|
|
119
133
|
|
|
@@ -78,13 +78,14 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
78
78
|
# ═══════════════════════════════════════════════════════════════════════
|
|
79
79
|
|
|
80
80
|
describe 'R1 · Reward.judge (Outcome Reward Model)' do
|
|
81
|
-
it '
|
|
81
|
+
it 'retains heuristic diagnostics without feeding the sentinel' do
|
|
82
82
|
v = reward.judge(request: 'scan the host', final: 'done — 3 hosts up',
|
|
83
83
|
trace: [ok_trace, ok_trace], proxy_ok: true)
|
|
84
84
|
expect(v[:score]).to be_between(0.0, 1.0)
|
|
85
85
|
expect(%i[solved partial wrong unknown]).to include(v[:verdict])
|
|
86
86
|
expect(v[:success]).to eq(v[:source].to_s != 'heuristic' && v[:score] >= 0.6)
|
|
87
|
-
expect(
|
|
87
|
+
expect(v[:training_score]).to be_nil
|
|
88
|
+
expect(reward.sentinel[:samples]).to eq 0
|
|
88
89
|
end
|
|
89
90
|
|
|
90
91
|
it 'floors self-reported failure language at 0.0' do
|
|
@@ -113,6 +114,7 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
113
114
|
describe 'R3 · Reward.sentinel (reward-hacking guard)' do
|
|
114
115
|
it 'flags proxy↔judge divergence >SENTINEL_GAP as a Mistake(tool: reward_signal)' do
|
|
115
116
|
stub_const('PWN::AI::Agent::Reward::SENTINEL_WINDOW', 5)
|
|
117
|
+
allow(reward).to receive(:llm_judge).and_return(score: 0.1, source: :llm_orm, rationale: 'independently judged failure')
|
|
116
118
|
5.times do
|
|
117
119
|
reward.judge(request: 'x', final: 'ok', trace: [bad_trace, bad_trace], proxy_ok: true)
|
|
118
120
|
end
|
|
@@ -314,7 +316,7 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
314
316
|
allow(curriculum).to receive(:reflect_available?).and_return(false)
|
|
315
317
|
# 2.4 — N≥2 solved holdouts required; practice bumps prompts_per to ≥2
|
|
316
318
|
allow(curriculum).to receive(:self_play).and_return(
|
|
317
|
-
score: 0.85, verdict: :solved,
|
|
319
|
+
score: 0.85, verdict: :solved, success: true, training_score: 0.85,
|
|
318
320
|
final: 'use `nmap` (typo: nmpa→nmap)',
|
|
319
321
|
prompt: 'fix nmap typo',
|
|
320
322
|
trace: "shell → nmap -sn 10.0.0.0/24\nshell → true\npwn_eval → :ok"
|
|
@@ -611,6 +613,7 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
611
613
|
PWN::Sessions.append(session_id: s[:id], role: 'user', content: 'enumerate hosts')
|
|
612
614
|
PWN::Sessions.append(session_id: s[:id], role: 'tool', content: "shell → #{ok_trace}")
|
|
613
615
|
|
|
616
|
+
allow(reward).to receive(:llm_judge).and_return(score: 0.8, source: :llm_orm, verdict: :solved, rationale: 'request covered')
|
|
614
617
|
expect(reward).to receive(:judge).and_call_original
|
|
615
618
|
expect(reward).to receive(:prm).and_call_original
|
|
616
619
|
expect(reward).to receive(:sentinel).and_call_original
|
|
@@ -643,7 +646,7 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
643
646
|
expect(row[:score]).to be <= 0.3
|
|
644
647
|
end
|
|
645
648
|
|
|
646
|
-
it '
|
|
649
|
+
it 'keeps a high judge score diagnostic when the critic disagrees rather than training the conflict' do
|
|
647
650
|
@agent_cfg[:auto_introspect] = true
|
|
648
651
|
@agent_cfg[:critic] = true
|
|
649
652
|
allow(curriculum).to receive(:critic).and_return(verdict: :flaw, flaw: 'plan_cover_low')
|
|
@@ -654,8 +657,10 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
654
657
|
s = PWN::Sessions.create(title: 'e2e_critic_keep')
|
|
655
658
|
learning.auto_introspect(session_id: s[:id], request: 'x', final: 'path-backed complete answer')
|
|
656
659
|
row = learning.outcomes.first
|
|
657
|
-
expect(row[:success]).to
|
|
658
|
-
expect(row[:score]).to
|
|
660
|
+
expect(row[:success]).to be_nil
|
|
661
|
+
expect(row[:score]).to eq(0.87)
|
|
662
|
+
expect(row[:verdict].to_s).to eq('unknown')
|
|
663
|
+
expect(row[:training_score]).to be_nil
|
|
659
664
|
end
|
|
660
665
|
end
|
|
661
666
|
|
|
@@ -751,11 +756,11 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
751
756
|
evalset = Array(o[:evalset])
|
|
752
757
|
if evalset.any? { |e| e[:signature].to_s.start_with?('smoke_') }
|
|
753
758
|
# smoke equal
|
|
754
|
-
{ resolved: 3, mean_score: 0.9, scores: [0.9, 0.9, 0.9] }
|
|
759
|
+
{ resolved: 3, mean_score: 0.9, scores: [0.9, 0.9, 0.9], unknown: 0 }
|
|
755
760
|
elsif tag.include?('cand') || tag == 'cand'
|
|
756
|
-
{ resolved: 8, mean_score: 0.85, scores: [0.85] * 10 }
|
|
761
|
+
{ resolved: 8, mean_score: 0.85, scores: [0.85] * 10, unknown: 0 }
|
|
757
762
|
else
|
|
758
|
-
{ resolved: 5, mean_score: 0.70, scores: [0.7] * 10 }
|
|
763
|
+
{ resolved: 5, mean_score: 0.70, scores: [0.7] * 10, unknown: 0 }
|
|
759
764
|
end
|
|
760
765
|
end
|
|
761
766
|
allow(curriculum).to receive(:smoke_eval_set).and_return(
|
|
@@ -776,14 +781,14 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
776
781
|
if evalset.any? { |e| e[:signature].to_s.start_with?('smoke_') }
|
|
777
782
|
tag = o[:tag].to_s
|
|
778
783
|
if tag == 'cand'
|
|
779
|
-
{ resolved: 0, mean_score: 0.1, scores: [0.1] }
|
|
784
|
+
{ resolved: 0, mean_score: 0.1, scores: [0.1], unknown: 0 }
|
|
780
785
|
else
|
|
781
|
-
{ resolved: 3, mean_score: 0.9, scores: [0.9] }
|
|
786
|
+
{ resolved: 3, mean_score: 0.9, scores: [0.9], unknown: 0 }
|
|
782
787
|
end
|
|
783
788
|
elsif o[:tag].to_s == 'cand'
|
|
784
|
-
{ resolved: 9, mean_score: 0.95, scores: [0.95] * 10 }
|
|
789
|
+
{ resolved: 9, mean_score: 0.95, scores: [0.95] * 10, unknown: 0 }
|
|
785
790
|
else
|
|
786
|
-
{ resolved: 5, mean_score: 0.70, scores: [0.7] * 10 }
|
|
791
|
+
{ resolved: 5, mean_score: 0.70, scores: [0.7] * 10, unknown: 0 }
|
|
787
792
|
end
|
|
788
793
|
end
|
|
789
794
|
allow(curriculum).to receive(:smoke_eval_set).and_return(
|
|
@@ -856,6 +861,7 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
856
861
|
PWN::Sessions.append(session_id: s[:id], role: 'tool', content: ok_trace)
|
|
857
862
|
PWN::Sessions.append(session_id: s[:id], role: 'assistant', content: '3 hosts up')
|
|
858
863
|
|
|
864
|
+
allow(reward).to receive(:llm_judge).and_return(score: 0.8, source: :llm_orm, verdict: :solved, rationale: 'trace matches request')
|
|
859
865
|
r = curriculum.offline_judge(since_hours: 24, limit: 10, prm: true, commit: true)
|
|
860
866
|
expect(r[:scored]).to be >= 1
|
|
861
867
|
row = learning.outcomes(tag: 'offline_judge').first
|
|
@@ -928,6 +934,7 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
928
934
|
allow(curriculum).to receive(:self_play).and_return(
|
|
929
935
|
score: 0.9, verdict: :solved,
|
|
930
936
|
final: "fixed with nmap\nsecond line\nthird",
|
|
937
|
+
success: true, training_score: 0.9,
|
|
931
938
|
prompt: 'scan the lab safely',
|
|
932
939
|
trace: "shell → nmap -sn 10.0.0.0/24\nshell → true"
|
|
933
940
|
)
|