pwn 0.5.613 → 0.5.617
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 +10 -10
- data/README.md +117 -17
- data/bin/pwn +47 -2
- data/bin/pwn_gqrx_scanner +75 -18
- data/documentation/AI-Integration.md +36 -0
- data/documentation/BurpSuite.md +26 -0
- data/documentation/Contributing.md +31 -0
- data/documentation/Diagrams.md +140 -0
- data/documentation/Drivers.md +25 -0
- data/documentation/General-PWN-Usage.md +60 -0
- data/documentation/Home.md +78 -0
- data/documentation/How-PWN-Works.md +62 -0
- data/documentation/Installation.md +75 -0
- data/documentation/NmapIt.md +18 -0
- data/documentation/Plugins.md +59 -0
- data/documentation/Reporting.md +28 -0
- data/documentation/SAST.md +32 -0
- data/documentation/Skills-Memory-Learning.md +44 -0
- data/documentation/Transparent-Browser.md +32 -0
- data/documentation/Troubleshooting.md +38 -0
- data/documentation/What-is-PWN.md +53 -0
- data/documentation/Why-PWN.md +48 -0
- data/documentation/diagrams/ai-integration-tool-calling.svg +172 -0
- data/documentation/diagrams/build.sh +20 -0
- data/documentation/diagrams/burp-vs-zap-preference.svg +69 -0
- data/documentation/diagrams/code-scanning-sast.svg +172 -0
- data/documentation/diagrams/dot/_THEME.md +27 -0
- data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
- data/documentation/diagrams/dot/ai-integration-tool-calling.dot +50 -0
- data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
- data/documentation/diagrams/dot/burp-vs-zap-preference.dot +26 -0
- data/documentation/diagrams/dot/code-scanning-sast.dot +49 -0
- data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
- data/documentation/diagrams/dot/driver-framework.dot +39 -0
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
- data/documentation/diagrams/dot/fuzzing-workflow.dot +47 -0
- data/documentation/diagrams/dot/history-to-drivers.dot +34 -0
- data/documentation/diagrams/dot/memory-skills-detailed.dot +60 -0
- data/documentation/diagrams/dot/network-infra-testing.dot +54 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +115 -0
- data/documentation/diagrams/dot/penetration-testing-workflow.dot +60 -0
- data/documentation/diagrams/dot/plugin-ecosystem.dot +45 -0
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +98 -0
- data/documentation/diagrams/dot/pwn-repl-prototyping.dot +56 -0
- data/documentation/diagrams/dot/reporting-pipeline.dot +46 -0
- data/documentation/diagrams/dot/reverse-engineering-flow.dot +43 -0
- data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
- data/documentation/diagrams/dot/sessions-cron-automation.dot +44 -0
- data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
- data/documentation/diagrams/dot/web-application-testing.dot +49 -0
- data/documentation/diagrams/dot/zero-day-research-flow.dot +49 -0
- data/documentation/diagrams/driver-framework.svg +111 -0
- data/documentation/diagrams/fuzzing-workflow.svg +160 -0
- data/documentation/diagrams/history-to-drivers.svg +119 -0
- data/documentation/diagrams/memory-skills-detailed.svg +202 -0
- data/documentation/diagrams/network-infra-testing.svg +183 -0
- data/documentation/diagrams/overall-pwn-architecture.svg +405 -0
- data/documentation/diagrams/penetration-testing-workflow.svg +201 -0
- data/documentation/diagrams/plugin-ecosystem.svg +276 -0
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +306 -0
- data/documentation/diagrams/pwn-repl-prototyping.svg +168 -0
- data/documentation/diagrams/reporting-pipeline.svg +151 -0
- data/documentation/diagrams/reverse-engineering-flow.svg +134 -0
- data/documentation/diagrams/sessions-cron-automation.svg +136 -0
- data/documentation/diagrams/web-application-testing.svg +173 -0
- data/documentation/diagrams/zero-day-research-flow.svg +153 -0
- data/documentation/pwn-REPL.md +33 -0
- data/documentation/pwn-ai-Agent.md +43 -0
- data/git_commit.sh +1 -1
- data/lib/pwn/ai/agent/assembly.rb +1 -1
- data/lib/pwn/ai/agent/btc.rb +1 -1
- data/lib/pwn/ai/agent/burp_suite.rb +1 -1
- data/lib/pwn/ai/agent/extrospection.rb +618 -0
- data/lib/pwn/ai/agent/gqrx.rb +2 -2
- data/lib/pwn/ai/agent/hacker_one.rb +1 -1
- data/lib/pwn/ai/agent/introspection.rb +91 -0
- data/lib/pwn/ai/agent/learning.rb +379 -0
- data/lib/pwn/ai/agent/loop.rb +33 -3
- data/lib/pwn/ai/agent/metrics.rb +155 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +32 -3
- data/lib/pwn/ai/agent/sast.rb +1 -1
- data/lib/pwn/ai/agent/swarm.rb +437 -0
- data/lib/pwn/ai/agent/tools/cron.rb +163 -0
- data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
- data/lib/pwn/ai/agent/tools/learning.rb +221 -0
- data/lib/pwn/ai/agent/tools/memory.rb +27 -0
- data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
- data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
- data/lib/pwn/ai/agent/tools/skills.rb +124 -8
- data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
- data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
- data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
- data/lib/pwn/ai/agent.rb +5 -0
- data/lib/pwn/ai/anthropic.rb +19 -4
- data/lib/pwn/ai/grok.rb +249 -106
- data/lib/pwn/ai.rb +0 -1
- data/lib/pwn/config.rb +96 -36
- data/lib/pwn/cron.rb +16 -7
- data/lib/pwn/plugins/monkey_patch.rb +2 -2
- data/lib/pwn/plugins/repl.rb +158 -284
- data/lib/pwn/sdr/decoder/base.rb +251 -0
- data/lib/pwn/sdr/decoder/gsm.rb +84 -185
- data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
- data/lib/pwn/sdr/decoder.rb +1 -0
- data/lib/pwn/sdr/gqrx.rb +446 -65
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +68 -0
- data/spec/lib/pwn/ai/agent/metrics_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
- data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
- data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
- data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +30 -0
- data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
- data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
- data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
- data/third_party/pwn_rdoc.jsonl +178 -33
- metadata +110 -36
- data/documentation/PWN.png +0 -0
- data/documentation/PWN_Contributors_and_Users.png +0 -0
- data/documentation/fax-spectrogram.png +0 -0
- data/documentation/fax-waveform.png +0 -0
- data/documentation/frozen_string_literal_files_changed_from_false_to_true.txt +0 -54
- data/documentation/lifecycle_authz_replay.example.yaml +0 -27
- data/documentation/pwn_android_war_dialer_session.png +0 -0
- data/documentation/pwn_install.png +0 -0
- data/documentation/pwn_wallpaper.jpg +0 -0
- data/documentation/ringing-spectrogram.png +0 -0
- data/documentation/ringing-waveform.png +0 -0
- data/documentation/vulnerability_report_template.md +0 -37
- data/lib/pwn/ai/introspection.rb +0 -76
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module PWN
|
|
6
|
+
module AI
|
|
7
|
+
module Agent
|
|
8
|
+
# PWN::AI::Agent::Introspection is the inward-facing counterpart to
|
|
9
|
+
# PWN::AI::Agent::Extrospection. Where Extrospection looks OUTWARD at
|
|
10
|
+
# the world the agent operates in (host state, toolchain, network,
|
|
11
|
+
# threat-intel), Introspection looks INWARD - it lets pwn hand a
|
|
12
|
+
# request to the active AI engine and reflect on its own artifacts,
|
|
13
|
+
# transcripts, findings, code, or decisions.
|
|
14
|
+
#
|
|
15
|
+
# This module is gated by `PWN::Env[:ai][:introspection]` so that
|
|
16
|
+
# potentially-sensitive local data is never shipped to a remote LLM
|
|
17
|
+
# unless the operator has explicitly opted in via pwn-vault / config.
|
|
18
|
+
#
|
|
19
|
+
# It is the single choke-point every PWN::AI::Agent::* domain agent
|
|
20
|
+
# (Assembly, BurpSuite, GQRX, HackerOne, SAST, VulnGen, ...) routes
|
|
21
|
+
# through when it wants an LLM opinion on locally-produced data, and
|
|
22
|
+
# it is also what PWN::AI::Agent::Learning.reflect uses to distill
|
|
23
|
+
# session transcripts into durable PWN::Memory lessons.
|
|
24
|
+
module Introspection
|
|
25
|
+
# Supported Method Parameters::
|
|
26
|
+
# response = PWN::AI::Agent::Introspection.reflect_on(
|
|
27
|
+
# request: 'required - String - What you want the AI to reflect on',
|
|
28
|
+
# system_role_content: 'optional - context to set up the model behavior for reflection',
|
|
29
|
+
# spinner: 'optional - Boolean - Display spinner during operation (default: false)',
|
|
30
|
+
# suppress_pii_warning: 'optional - Boolean - Suppress PII Warnings (default: false)'
|
|
31
|
+
# )
|
|
32
|
+
|
|
33
|
+
public_class_method def self.reflect_on(opts = {})
|
|
34
|
+
request = opts[:request]
|
|
35
|
+
raise 'ERROR: request must be provided' if request.nil?
|
|
36
|
+
|
|
37
|
+
system_role_content = opts[:system_role_content]
|
|
38
|
+
|
|
39
|
+
spinner = opts[:spinner] || false
|
|
40
|
+
|
|
41
|
+
suppress_pii_warning = opts[:suppress_pii_warning] || false
|
|
42
|
+
|
|
43
|
+
response = nil
|
|
44
|
+
|
|
45
|
+
ai_introspection = PWN::Env[:ai][:introspection]
|
|
46
|
+
|
|
47
|
+
if ai_introspection && request.length.positive?
|
|
48
|
+
engine = PWN::Env[:ai][:active].to_s.downcase.to_sym
|
|
49
|
+
valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :agent }.map(&:downcase)
|
|
50
|
+
raise "ERROR: Unsupported AI engine. Supported engines are: #{valid_ai_engines}" unless valid_ai_engines.include?(engine)
|
|
51
|
+
|
|
52
|
+
warn "AI Introspection is enabled. Ensure #{engine} has been authorized for use and/or requests are sanitized properly." unless suppress_pii_warning
|
|
53
|
+
response = PWN::AI::Agent::Loop.run(
|
|
54
|
+
request: request.chomp,
|
|
55
|
+
system_role_content: system_role_content,
|
|
56
|
+
enabled_toolsets: [],
|
|
57
|
+
spinner: spinner
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
response
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
raise e
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
67
|
+
|
|
68
|
+
public_class_method def self.authors
|
|
69
|
+
"AUTHOR(S):
|
|
70
|
+
0day Inc. <support@0dayinc.com>
|
|
71
|
+
"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Display Usage for this Module
|
|
75
|
+
|
|
76
|
+
public_class_method def self.help
|
|
77
|
+
puts "USAGE:
|
|
78
|
+
#{self}.reflect_on(
|
|
79
|
+
request: 'required - String - What you want the AI to reflect on',
|
|
80
|
+
system_role_content: 'optional - context to set up the model behavior for reflection',
|
|
81
|
+
spinner: 'optional - Boolean - Display spinner during operation (default: false)',
|
|
82
|
+
suppress_pii_warning: 'optional - Boolean - Suppress PII Warnings (default: false)'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
#{self}.authors
|
|
86
|
+
"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
6
|
+
require 'digest'
|
|
7
|
+
|
|
8
|
+
module PWN
|
|
9
|
+
module AI
|
|
10
|
+
module Agent
|
|
11
|
+
# PWN::AI::Agent::Learning is the self-improvement engine that closes
|
|
12
|
+
# the pwn-ai feedback loop. It captures task outcomes, mines session
|
|
13
|
+
# transcripts for durable lessons, promotes successful workflows into
|
|
14
|
+
# reusable skills, and prunes / consolidates persistent memory so the
|
|
15
|
+
# agent gets sharper over time instead of accumulating noise.
|
|
16
|
+
#
|
|
17
|
+
# Data flows:
|
|
18
|
+
# Loop.run --(tool telemetry)--> Metrics.record
|
|
19
|
+
# Loop.run --(final answer)----> Learning.auto_reflect (opt-in)
|
|
20
|
+
# model --(tool calls)------> learning_note_outcome / _distill_skill
|
|
21
|
+
# PromptBuilder <----------------- Learning.to_context + Metrics.to_context
|
|
22
|
+
#
|
|
23
|
+
# Everything is file-backed under ~/.pwn so it survives across REPL
|
|
24
|
+
# restarts and is shared by every future session.
|
|
25
|
+
module Learning
|
|
26
|
+
LEARNING_FILE = File.join(Dir.home, '.pwn', 'learning.jsonl')
|
|
27
|
+
MAX_MEMORY_ENTRIES = 200
|
|
28
|
+
|
|
29
|
+
# Supported Method Parameters::
|
|
30
|
+
# entry = PWN::AI::Agent::Learning.note_outcome(
|
|
31
|
+
# task: 'required - short description of what was attempted',
|
|
32
|
+
# success: 'required - Boolean, did the attempt achieve its goal',
|
|
33
|
+
# details: 'optional - free-form notes / error / evidence',
|
|
34
|
+
# session_id: 'optional - PWN::Sessions id this outcome belongs to',
|
|
35
|
+
# tags: 'optional - Array of String labels for later retrieval'
|
|
36
|
+
# )
|
|
37
|
+
|
|
38
|
+
public_class_method def self.note_outcome(opts = {})
|
|
39
|
+
task = opts[:task].to_s
|
|
40
|
+
success = opts[:success] ? true : false
|
|
41
|
+
raise 'ERROR: task is required' if task.strip.empty?
|
|
42
|
+
|
|
43
|
+
entry = {
|
|
44
|
+
id: Digest::SHA256.hexdigest("#{task}-#{Time.now.to_f}")[0, 12],
|
|
45
|
+
task: task,
|
|
46
|
+
success: success,
|
|
47
|
+
details: opts[:details].to_s[0, 2_000],
|
|
48
|
+
session_id: opts[:session_id],
|
|
49
|
+
tags: Array(opts[:tags]).map(&:to_s),
|
|
50
|
+
timestamp: Time.now.utc.iso8601
|
|
51
|
+
}
|
|
52
|
+
FileUtils.mkdir_p(File.dirname(LEARNING_FILE))
|
|
53
|
+
File.open(LEARNING_FILE, 'a') { |f| f.puts(JSON.generate(entry)) }
|
|
54
|
+
|
|
55
|
+
key = :"lesson_#{entry[:id]}"
|
|
56
|
+
cat = :lesson
|
|
57
|
+
val = "#{success ? 'SUCCESS' : 'FAILURE'}: #{task} — #{opts[:details].to_s.strip[0, 200]}"
|
|
58
|
+
PWN::Memory.remember(key: key, value: val, category: cat) if defined?(PWN::Memory)
|
|
59
|
+
|
|
60
|
+
entry
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Supported Method Parameters::
|
|
64
|
+
# rows = PWN::AI::Agent::Learning.outcomes(
|
|
65
|
+
# limit: 'optional - max entries returned newest-first (default 50)',
|
|
66
|
+
# success: 'optional - filter by Boolean outcome',
|
|
67
|
+
# tag: 'optional - filter by tag substring'
|
|
68
|
+
# )
|
|
69
|
+
|
|
70
|
+
public_class_method def self.outcomes(opts = {})
|
|
71
|
+
limit = opts[:limit] || 50
|
|
72
|
+
want_ok = opts.key?(:success) ? !opts[:success].nil? && opts[:success] != false : nil
|
|
73
|
+
tag = opts[:tag].to_s.downcase
|
|
74
|
+
return [] unless File.exist?(LEARNING_FILE)
|
|
75
|
+
|
|
76
|
+
rows = File.readlines(LEARNING_FILE).map do |l|
|
|
77
|
+
JSON.parse(l, symbolize_names: true)
|
|
78
|
+
rescue StandardError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
rows.compact!
|
|
82
|
+
rows.select! { |r| r[:success] == want_ok } unless want_ok.nil?
|
|
83
|
+
rows.select! { |r| Array(r[:tags]).any? { |t| t.to_s.downcase.include?(tag) } } unless tag.empty?
|
|
84
|
+
rows.reverse.first(limit)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Supported Method Parameters::
|
|
88
|
+
# stats = PWN::AI::Agent::Learning.stats
|
|
89
|
+
|
|
90
|
+
public_class_method def self.stats
|
|
91
|
+
rows = outcomes(limit: 10_000)
|
|
92
|
+
total = rows.length
|
|
93
|
+
ok = rows.count { |r| r[:success] }
|
|
94
|
+
skills = defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) ? PWN::Skills.keys.length : 0
|
|
95
|
+
mem = defined?(PWN::Memory) ? PWN::Memory.load.keys.length : 0
|
|
96
|
+
{
|
|
97
|
+
total_outcomes: total,
|
|
98
|
+
successes: ok,
|
|
99
|
+
failures: total - ok,
|
|
100
|
+
success_rate: total.positive? ? (ok.to_f / total).round(3) : 0.0,
|
|
101
|
+
skills_known: skills,
|
|
102
|
+
memory_entries: mem,
|
|
103
|
+
tool_metrics: (Metrics.summary(limit: 5) if defined?(Metrics)),
|
|
104
|
+
extrospection: (Extrospection.stats if defined?(Extrospection))
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Supported Method Parameters::
|
|
109
|
+
# ctx = PWN::AI::Agent::Learning.to_context(
|
|
110
|
+
# limit: 'optional - number of recent outcomes to surface (default 5)'
|
|
111
|
+
# )
|
|
112
|
+
|
|
113
|
+
public_class_method def self.to_context(opts = {})
|
|
114
|
+
limit = opts[:limit] || 5
|
|
115
|
+
rows = outcomes(limit: limit)
|
|
116
|
+
return '' if rows.empty?
|
|
117
|
+
|
|
118
|
+
lines = rows.map do |r|
|
|
119
|
+
flag = r[:success] ? '✓' : '✗'
|
|
120
|
+
" #{flag} #{r[:task][0, 100]} (#{r[:timestamp]})"
|
|
121
|
+
end
|
|
122
|
+
s = stats
|
|
123
|
+
hdr = "RECENT OUTCOMES (success_rate=#{(s[:success_rate] * 100).round(1)}% over #{s[:total_outcomes]} attempts)"
|
|
124
|
+
"#{hdr}\n#{lines.join("\n")}\n\n"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Supported Method Parameters::
|
|
128
|
+
# skill = PWN::AI::Agent::Learning.distill_skill(
|
|
129
|
+
# name: 'required - snake_case name for the new skill',
|
|
130
|
+
# session_id: 'optional - PWN::Sessions id to mine (uses its transcript)',
|
|
131
|
+
# content: 'optional - explicit markdown body; overrides transcript mining',
|
|
132
|
+
# references: 'optional - Array of reference URLs / CWE / CVE / ATT&CK ids'
|
|
133
|
+
# )
|
|
134
|
+
|
|
135
|
+
public_class_method def self.distill_skill(opts = {})
|
|
136
|
+
name = opts[:name].to_s.gsub(/[^a-z0-9_-]/i, '_')
|
|
137
|
+
raise 'ERROR: name is required' if name.empty?
|
|
138
|
+
|
|
139
|
+
body = opts[:content].to_s
|
|
140
|
+
body = build_skill_from_session(session_id: opts[:session_id], name: name) if body.strip.empty? && opts[:session_id]
|
|
141
|
+
raise 'ERROR: content or session_id is required' if body.strip.empty?
|
|
142
|
+
|
|
143
|
+
refs = Array(opts[:references]).map(&:to_s).map(&:strip).reject(&:empty?).uniq
|
|
144
|
+
unless refs.empty?
|
|
145
|
+
body = "---\nreferences:\n#{refs.map { |r| " - #{r}" }.join("\n")}\n---\n#{body}" unless body.start_with?("---\n")
|
|
146
|
+
body = "#{body.rstrip}\n\n## References\n#{refs.map { |r| "- #{r}" }.join("\n")}\n" unless body =~ /^\#{1,3}\s*References\s*$/i
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
dir = skills_dir
|
|
150
|
+
FileUtils.mkdir_p(dir)
|
|
151
|
+
path = File.join(dir, "#{name}.md")
|
|
152
|
+
File.write(path, body)
|
|
153
|
+
PWN::Config.load_skills(pwn_skills_path: dir) if defined?(PWN::Config) && PWN::Config.respond_to?(:load_skills)
|
|
154
|
+
note_outcome(task: "distill_skill:#{name}", success: true, details: "Saved #{path}", tags: %w[skill auto])
|
|
155
|
+
{ saved: true, name: name, path: path, bytes: body.bytesize, references: refs }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Supported Method Parameters::
|
|
159
|
+
# report = PWN::AI::Agent::Learning.reflect(
|
|
160
|
+
# session_id: 'required - PWN::Sessions id to analyse',
|
|
161
|
+
# dry_run: 'optional - when true, do not write to Memory/Skills (default false)'
|
|
162
|
+
# )
|
|
163
|
+
#
|
|
164
|
+
# Uses PWN::AI::Agent::Introspection (when available) to LLM-summarise the
|
|
165
|
+
# session into structured lessons. Falls back to a heuristic
|
|
166
|
+
# extractor when introspection is disabled so learning never stops.
|
|
167
|
+
|
|
168
|
+
public_class_method def self.reflect(opts = {})
|
|
169
|
+
session_id = opts[:session_id]
|
|
170
|
+
dry_run = opts[:dry_run] ? true : false
|
|
171
|
+
raise 'ERROR: session_id is required' if session_id.to_s.empty?
|
|
172
|
+
|
|
173
|
+
transcript = PWN::Sessions.load(session_id: session_id)
|
|
174
|
+
return { session_id: session_id, lessons: [], reason: 'empty transcript' } if transcript.empty?
|
|
175
|
+
|
|
176
|
+
lessons = introspective_lessons(transcript: transcript)
|
|
177
|
+
lessons = heuristic_lessons(transcript: transcript) if lessons.empty?
|
|
178
|
+
|
|
179
|
+
saved = []
|
|
180
|
+
lessons.each do |l|
|
|
181
|
+
next if l.to_s.strip.empty?
|
|
182
|
+
|
|
183
|
+
key = :"reflect_#{session_id}_#{Digest::SHA256.hexdigest(l)[0, 8]}"
|
|
184
|
+
PWN::Memory.remember(key: key, value: l, category: :lesson) unless dry_run
|
|
185
|
+
saved << { key: key, lesson: l }
|
|
186
|
+
end
|
|
187
|
+
consolidate unless dry_run
|
|
188
|
+
|
|
189
|
+
{ session_id: session_id, lessons: saved, count: saved.length, dry_run: dry_run }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Supported Method Parameters::
|
|
193
|
+
# PWN::AI::Agent::Learning.auto_reflect(
|
|
194
|
+
# session_id: 'required - id of the just-completed session',
|
|
195
|
+
# request: 'optional - original user request (for outcome logging)',
|
|
196
|
+
# final: 'optional - final assistant answer (for outcome logging)'
|
|
197
|
+
# )
|
|
198
|
+
#
|
|
199
|
+
# Called by Loop.run when PWN::Env[:ai][:agent][:auto_reflect] is
|
|
200
|
+
# truthy. Never raises — learning must not break the primary loop.
|
|
201
|
+
|
|
202
|
+
public_class_method def self.auto_reflect(opts = {})
|
|
203
|
+
session_id = opts[:session_id]
|
|
204
|
+
return unless session_id
|
|
205
|
+
return unless auto_reflect_enabled?
|
|
206
|
+
|
|
207
|
+
note_outcome(
|
|
208
|
+
task: opts[:request].to_s[0, 120],
|
|
209
|
+
success: !opts[:final].to_s.strip.empty?,
|
|
210
|
+
details: opts[:final].to_s[0, 300],
|
|
211
|
+
session_id: session_id,
|
|
212
|
+
tags: %w[auto loop]
|
|
213
|
+
)
|
|
214
|
+
reflect(session_id: session_id)
|
|
215
|
+
Extrospection.auto_extrospect(session_id: session_id) if defined?(Extrospection)
|
|
216
|
+
rescue StandardError => e
|
|
217
|
+
warn "[pwn-ai/learning] auto_reflect swallowed: #{e.class}: #{e.message}"
|
|
218
|
+
nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Supported Method Parameters::
|
|
222
|
+
# removed = PWN::AI::Agent::Learning.consolidate(
|
|
223
|
+
# max_entries: 'optional - hard cap on PWN::Memory size (default MAX_MEMORY_ENTRIES)'
|
|
224
|
+
# )
|
|
225
|
+
#
|
|
226
|
+
# Deduplicates near-identical lesson values and prunes the oldest
|
|
227
|
+
# entries once the cap is exceeded so the injected MEMORY block
|
|
228
|
+
# stays high-signal.
|
|
229
|
+
|
|
230
|
+
public_class_method def self.consolidate(opts = {})
|
|
231
|
+
cap = opts[:max_entries] || MAX_MEMORY_ENTRIES
|
|
232
|
+
return { removed: 0 } unless defined?(PWN::Memory)
|
|
233
|
+
|
|
234
|
+
mem = PWN::Memory.load
|
|
235
|
+
seen = {}
|
|
236
|
+
removed = []
|
|
237
|
+
mem.each do |k, v|
|
|
238
|
+
sig = Digest::SHA256.hexdigest(v[:value].to_s.strip.downcase)[0, 16]
|
|
239
|
+
if seen[sig]
|
|
240
|
+
removed << k
|
|
241
|
+
else
|
|
242
|
+
seen[sig] = k
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
removed.each { |k| mem.delete(k) }
|
|
246
|
+
|
|
247
|
+
if mem.size > cap
|
|
248
|
+
sorted = mem.sort_by { |_k, v| v[:timestamp].to_s }
|
|
249
|
+
drop = sorted.first(mem.size - cap).map(&:first)
|
|
250
|
+
drop.each { |k| mem.delete(k) }
|
|
251
|
+
removed.concat(drop)
|
|
252
|
+
end
|
|
253
|
+
PWN::Memory.save(mem: mem)
|
|
254
|
+
{ removed: removed.length, remaining: mem.size }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Supported Method Parameters::
|
|
258
|
+
# PWN::AI::Agent::Learning.reset
|
|
259
|
+
|
|
260
|
+
public_class_method def self.reset
|
|
261
|
+
FileUtils.rm_f(LEARNING_FILE)
|
|
262
|
+
{ cleared: true }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# -------------------------------------------------------------
|
|
266
|
+
# privates
|
|
267
|
+
# -------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
private_class_method def self.auto_reflect_enabled?
|
|
270
|
+
return false unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
271
|
+
|
|
272
|
+
PWN::Env.dig(:ai, :agent, :auto_reflect) ? true : false
|
|
273
|
+
rescue StandardError
|
|
274
|
+
false
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
private_class_method def self.skills_dir
|
|
278
|
+
if defined?(PWN::Config) && PWN::Config.respond_to?(:pwn_skills_path)
|
|
279
|
+
PWN::Config.pwn_skills_path
|
|
280
|
+
else
|
|
281
|
+
File.join(Dir.home, '.pwn', 'skills')
|
|
282
|
+
end
|
|
283
|
+
rescue StandardError
|
|
284
|
+
File.join(Dir.home, '.pwn', 'skills')
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
private_class_method def self.transcript_text(opts = {})
|
|
288
|
+
transcript = opts[:transcript] || []
|
|
289
|
+
transcript.map do |e|
|
|
290
|
+
"[#{e[:role]}] #{e[:content].to_s.gsub(/\s+/, ' ')[0, 400]}"
|
|
291
|
+
end.join("\n")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
private_class_method def self.introspective_lessons(opts = {})
|
|
295
|
+
transcript = opts[:transcript] || []
|
|
296
|
+
return [] unless defined?(PWN::AI::Agent::Introspection)
|
|
297
|
+
return [] unless defined?(PWN::Env) && PWN::Env.is_a?(Hash) && PWN::Env.dig(:ai, :introspection)
|
|
298
|
+
|
|
299
|
+
req = "Analyse this pwn-ai session transcript and emit up to 5 durable, generalizable lessons (one per line, no numbering, imperative voice) that would make future runs faster or more reliable. Focus on tool selection, error recovery, and target-agnostic technique. Ignore trivia.\n\nTRANSCRIPT:\n#{transcript_text(transcript: transcript)}"
|
|
300
|
+
resp = PWN::AI::Agent::Introspection.reflect_on(request: req, suppress_pii_warning: true)
|
|
301
|
+
resp.to_s.lines.map(&:strip).reject(&:empty?).first(5)
|
|
302
|
+
rescue StandardError
|
|
303
|
+
[]
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
private_class_method def self.heuristic_lessons(opts = {})
|
|
307
|
+
transcript = opts[:transcript] || []
|
|
308
|
+
lessons = []
|
|
309
|
+
transcript.each do |e|
|
|
310
|
+
c = e[:content].to_s
|
|
311
|
+
next unless e[:role].to_s == 'tool'
|
|
312
|
+
|
|
313
|
+
if c.include?('"success":false') || c.match?(/error|Error|ERROR/)
|
|
314
|
+
tool = c[/^(\w+) →/, 1] || c[/"error":"([^"]{5,120})"/, 1] || 'tool'
|
|
315
|
+
lessons << "Avoid repeating failure pattern from #{tool}: #{c[0, 160]}"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
if lessons.empty?
|
|
319
|
+
asst = transcript.rfind { |e| e[:role].to_s == 'assistant' }
|
|
320
|
+
lessons << "Approach that worked: #{asst[:content].to_s.strip[0, 200]}" if asst && !asst[:content].to_s.strip.empty?
|
|
321
|
+
end
|
|
322
|
+
lessons.uniq.first(5)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private_class_method def self.build_skill_from_session(opts = {})
|
|
326
|
+
session_id = opts[:session_id]
|
|
327
|
+
name = opts[:name]
|
|
328
|
+
transcript = PWN::Sessions.load(session_id: session_id)
|
|
329
|
+
steps = transcript.select { |e| %w[tool assistant].include?(e[:role].to_s) }
|
|
330
|
+
.map { |e| "- **#{e[:role]}**: #{e[:content].to_s.strip[0, 300]}" }
|
|
331
|
+
user = transcript.find { |e| e[:role].to_s == 'user' }
|
|
332
|
+
goal = user ? user[:content].to_s.strip[0, 200] : name
|
|
333
|
+
<<~MD
|
|
334
|
+
# #{name.tr('_-', ' ').capitalize}
|
|
335
|
+
|
|
336
|
+
_Auto-distilled by PWN::AI::Agent::Learning from session `#{session_id}` on #{Time.now.utc.iso8601}._
|
|
337
|
+
|
|
338
|
+
## Goal
|
|
339
|
+
#{goal}
|
|
340
|
+
|
|
341
|
+
## Observed Procedure
|
|
342
|
+
#{steps.join("\n")}
|
|
343
|
+
|
|
344
|
+
## Notes
|
|
345
|
+
Refine this skill by editing #{name}.md under ~/.pwn/skills.
|
|
346
|
+
MD
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
350
|
+
|
|
351
|
+
public_class_method def self.authors
|
|
352
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Display Usage for this Module
|
|
356
|
+
|
|
357
|
+
public_class_method def self.help
|
|
358
|
+
puts <<~USAGE
|
|
359
|
+
USAGE:
|
|
360
|
+
PWN::AI::Agent::Learning.note_outcome(task: 'nmap sweep 10.0.0.0/24', success: true, details: '12 hosts up')
|
|
361
|
+
PWN::AI::Agent::Learning.outcomes(limit: 20, success: false)
|
|
362
|
+
PWN::AI::Agent::Learning.reflect(session_id: sid) # LLM or heuristic → PWN::Memory
|
|
363
|
+
PWN::AI::Agent::Learning.auto_reflect(session_id: sid, request: req, final: text)
|
|
364
|
+
PWN::AI::Agent::Learning.distill_skill(name: 'quick_recon', session_id: sid)
|
|
365
|
+
PWN::AI::Agent::Learning.consolidate(max_entries: 200) # dedupe + prune Memory
|
|
366
|
+
PWN::AI::Agent::Learning.to_context(limit: 5) # injected by PromptBuilder
|
|
367
|
+
PWN::AI::Agent::Learning.stats
|
|
368
|
+
PWN::AI::Agent::Learning.reset
|
|
369
|
+
|
|
370
|
+
Enable end-of-run auto-learning with:
|
|
371
|
+
PWN::Env[:ai][:agent][:auto_reflect] = true
|
|
372
|
+
|
|
373
|
+
#{self}.authors
|
|
374
|
+
USAGE
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -49,6 +49,32 @@ module PWN
|
|
|
49
49
|
DEFAULT_MAX_ITERS
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
private_class_method def self.record_metrics(opts = {})
|
|
53
|
+
name = opts[:name]
|
|
54
|
+
started = opts[:started]
|
|
55
|
+
raw = opts[:raw].to_s
|
|
56
|
+
ok = raw.include?('"success":true')
|
|
57
|
+
err = raw[/"error":"([^"]{1,300})"/, 1]
|
|
58
|
+
dur = started ? (Time.now - started) : 0.0
|
|
59
|
+
Metrics.record(name: name, success: ok, duration: dur, error: err) if defined?(Metrics)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Stash the active session_id under PWN::Env[:ai][:session_id] so
|
|
65
|
+
# tool handlers (sessions_current) can discover it without a Pry
|
|
66
|
+
# dependency. PWN::Env is frozen at the top level but [:ai] is a
|
|
67
|
+
# nested mutable Hash on all supported config paths — swallow if not.
|
|
68
|
+
private_class_method def self.expose_current_session(opts = {})
|
|
69
|
+
sid = opts[:session_id]
|
|
70
|
+
return unless sid && defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
ai = PWN::Env[:ai]
|
|
73
|
+
ai[:session_id] = sid if ai.is_a?(Hash) && !ai.frozen?
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
52
78
|
private_class_method def self.append_session(opts = {})
|
|
53
79
|
session_id = opts[:session_id]
|
|
54
80
|
return unless session_id && defined?(PWN::Sessions)
|
|
@@ -145,6 +171,7 @@ module PWN
|
|
|
145
171
|
system_role_content = opts[:system_role_content] ||= PWN::AI::Agent::PromptBuilder.build(session_id: session_id)
|
|
146
172
|
|
|
147
173
|
Registry.discover
|
|
174
|
+
expose_current_session(session_id: session_id)
|
|
148
175
|
|
|
149
176
|
tools = Registry.definitions(enabled: opts[:enabled_toolsets])
|
|
150
177
|
messages = [
|
|
@@ -163,13 +190,16 @@ module PWN
|
|
|
163
190
|
if calls.empty?
|
|
164
191
|
text = msg[:content].to_s
|
|
165
192
|
append_session(session_id: session_id, role: 'assistant', content: text)
|
|
193
|
+
Learning.auto_reflect(session_id: session_id, request: request, final: text) if defined?(Learning)
|
|
166
194
|
return text
|
|
167
195
|
end
|
|
168
196
|
|
|
169
197
|
calls.each do |tc|
|
|
170
|
-
name
|
|
171
|
-
entry
|
|
172
|
-
|
|
198
|
+
name = tc.dig(:function, :name).to_s
|
|
199
|
+
entry = Registry.lookup(name: name)
|
|
200
|
+
started = Time.now
|
|
201
|
+
raw = Dispatch.call(tool_call: tc)
|
|
202
|
+
record_metrics(name: name, started: started, raw: raw)
|
|
173
203
|
result = Result.condition(content: raw, entry: entry)
|
|
174
204
|
|
|
175
205
|
on_tool&.call(name, tc.dig(:function, :arguments), result)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module PWN
|
|
8
|
+
module AI
|
|
9
|
+
module Agent
|
|
10
|
+
# PWN::AI::Agent::Metrics is the telemetry layer of the pwn-ai
|
|
11
|
+
# learning loop. Every tool dispatch performed by
|
|
12
|
+
# PWN::AI::Agent::Loop is recorded here (name, success, duration,
|
|
13
|
+
# last error) and persisted to ~/.pwn/metrics.json.
|
|
14
|
+
#
|
|
15
|
+
# PromptBuilder re-injects a compact effectiveness summary into the
|
|
16
|
+
# system prompt on every turn, so the model gains awareness of which
|
|
17
|
+
# tools historically succeed vs. fail on THIS host and can adapt its
|
|
18
|
+
# tool selection accordingly. This is one half of the closed
|
|
19
|
+
# feedback loop that lets pwn-ai continuously make itself smarter
|
|
20
|
+
# (the other half is PWN::AI::Agent::Learning).
|
|
21
|
+
module Metrics
|
|
22
|
+
METRICS_FILE = File.join(Dir.home, '.pwn', 'metrics.json')
|
|
23
|
+
|
|
24
|
+
# Supported Method Parameters::
|
|
25
|
+
# metrics = PWN::AI::Agent::Metrics.load
|
|
26
|
+
|
|
27
|
+
public_class_method def self.load
|
|
28
|
+
FileUtils.mkdir_p(File.dirname(METRICS_FILE))
|
|
29
|
+
return { tools: {}, updated_at: nil } unless File.exist?(METRICS_FILE)
|
|
30
|
+
|
|
31
|
+
JSON.parse(File.read(METRICS_FILE), symbolize_names: true)
|
|
32
|
+
rescue StandardError
|
|
33
|
+
{ tools: {}, updated_at: nil }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Supported Method Parameters::
|
|
37
|
+
# PWN::AI::Agent::Metrics.save(
|
|
38
|
+
# metrics: 'required - Hash returned by .load / mutated in place'
|
|
39
|
+
# )
|
|
40
|
+
|
|
41
|
+
public_class_method def self.save(opts = {})
|
|
42
|
+
metrics = opts[:metrics] ||= { tools: {} }
|
|
43
|
+
metrics[:updated_at] = Time.now.utc.iso8601
|
|
44
|
+
FileUtils.mkdir_p(File.dirname(METRICS_FILE))
|
|
45
|
+
File.write(METRICS_FILE, JSON.pretty_generate(metrics))
|
|
46
|
+
metrics
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Supported Method Parameters::
|
|
50
|
+
# PWN::AI::Agent::Metrics.record(
|
|
51
|
+
# name: 'required - tool name that was dispatched',
|
|
52
|
+
# success: 'required - Boolean, did the handler complete without error',
|
|
53
|
+
# duration: 'optional - Float seconds the dispatch took',
|
|
54
|
+
# error: 'optional - String error message when success is false'
|
|
55
|
+
# )
|
|
56
|
+
|
|
57
|
+
public_class_method def self.record(opts = {})
|
|
58
|
+
name = opts[:name].to_s
|
|
59
|
+
success = opts[:success] ? true : false
|
|
60
|
+
duration = opts[:duration].to_f
|
|
61
|
+
error = opts[:error]
|
|
62
|
+
return if name.empty?
|
|
63
|
+
|
|
64
|
+
metrics = load
|
|
65
|
+
metrics[:tools] ||= {}
|
|
66
|
+
key = name.to_sym
|
|
67
|
+
t = metrics[:tools][key] ||= {
|
|
68
|
+
calls: 0, ok: 0, fail: 0, total_duration: 0.0,
|
|
69
|
+
last_error: nil, last_at: nil
|
|
70
|
+
}
|
|
71
|
+
t[:calls] += 1
|
|
72
|
+
t[:ok] += 1 if success
|
|
73
|
+
t[:fail] += 1 unless success
|
|
74
|
+
t[:total_duration] += duration
|
|
75
|
+
t[:last_error] = error.to_s[0, 300] if error && !success
|
|
76
|
+
t[:last_at] = Time.now.utc.iso8601
|
|
77
|
+
save(metrics: metrics)
|
|
78
|
+
t
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Supported Method Parameters::
|
|
82
|
+
# rows = PWN::AI::Agent::Metrics.summary(
|
|
83
|
+
# limit: 'optional - cap number of tools returned (default 25)'
|
|
84
|
+
# )
|
|
85
|
+
|
|
86
|
+
public_class_method def self.summary(opts = {})
|
|
87
|
+
limit = opts[:limit] || 25
|
|
88
|
+
tools = load[:tools] || {}
|
|
89
|
+
rows = tools.map do |name, t|
|
|
90
|
+
calls = t[:calls].to_i
|
|
91
|
+
ok = t[:ok].to_i
|
|
92
|
+
rate = calls.positive? ? (ok.to_f / calls).round(3) : 0.0
|
|
93
|
+
avg = calls.positive? ? (t[:total_duration].to_f / calls).round(3) : 0.0
|
|
94
|
+
{
|
|
95
|
+
name: name.to_s,
|
|
96
|
+
calls: calls,
|
|
97
|
+
success_rate: rate,
|
|
98
|
+
avg_duration: avg,
|
|
99
|
+
last_error: t[:last_error],
|
|
100
|
+
last_at: t[:last_at]
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
rows.sort_by { |r| [-r[:calls], -r[:success_rate]] }.first(limit)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Supported Method Parameters::
|
|
107
|
+
# ctx = PWN::AI::Agent::Metrics.to_context(
|
|
108
|
+
# limit: 'optional - cap number of tools included (default 8)'
|
|
109
|
+
# )
|
|
110
|
+
|
|
111
|
+
public_class_method def self.to_context(opts = {})
|
|
112
|
+
limit = opts[:limit] || 8
|
|
113
|
+
rows = summary(limit: limit)
|
|
114
|
+
return '' if rows.empty?
|
|
115
|
+
|
|
116
|
+
lines = rows.map do |r|
|
|
117
|
+
err = r[:last_error] ? " last_err=#{r[:last_error][0, 60]}" : ''
|
|
118
|
+
" - #{r[:name]}: calls=#{r[:calls]} success=#{(r[:success_rate] * 100).round(1)}% avg=#{r[:avg_duration]}s#{err}"
|
|
119
|
+
end
|
|
120
|
+
"TOOL EFFECTIVENESS (historical, adapt tool choice accordingly)\n#{lines.join("\n")}\n\n"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Supported Method Parameters::
|
|
124
|
+
# PWN::AI::Agent::Metrics.reset
|
|
125
|
+
|
|
126
|
+
public_class_method def self.reset
|
|
127
|
+
FileUtils.rm_f(METRICS_FILE)
|
|
128
|
+
{ tools: {}, updated_at: nil }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
132
|
+
|
|
133
|
+
public_class_method def self.authors
|
|
134
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Display Usage for this Module
|
|
138
|
+
|
|
139
|
+
public_class_method def self.help
|
|
140
|
+
puts <<~USAGE
|
|
141
|
+
USAGE:
|
|
142
|
+
PWN::AI::Agent::Metrics.record(name: 'shell', success: true, duration: 0.42)
|
|
143
|
+
PWN::AI::Agent::Metrics.summary(limit: 10)
|
|
144
|
+
PWN::AI::Agent::Metrics.to_context(limit: 8) # injected by PromptBuilder
|
|
145
|
+
PWN::AI::Agent::Metrics.reset
|
|
146
|
+
PWN::AI::Agent::Metrics.load
|
|
147
|
+
PWN::AI::Agent::Metrics.save(metrics: hash)
|
|
148
|
+
|
|
149
|
+
#{self}.authors
|
|
150
|
+
USAGE
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|