pwn 0.5.627 → 0.5.628
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/.rubocop_todo.yml +20 -30
- data/Gemfile +1 -1
- data/README.md +2 -2
- data/lib/pwn/ai/agent/dispatch.rb +76 -4
- data/lib/pwn/ai/agent/learning.rb +124 -1
- data/lib/pwn/ai/agent/loop.rb +104 -12
- data/lib/pwn/ai/agent/metrics.rb +60 -27
- data/lib/pwn/ai/agent/prompt_builder.rb +63 -12
- data/lib/pwn/ai/agent/reflect.rb +41 -7
- data/lib/pwn/ai/agent/registry.rb +69 -4
- data/lib/pwn/ai/ollama.rb +39 -10
- data/lib/pwn/config.rb +11 -0
- data/lib/pwn/ffi/liquid.rb +179 -0
- data/lib/pwn/memory.rb +15 -0
- data/lib/pwn/memory_index.rb +233 -0
- data/lib/pwn/sdr/decoder/bluetooth.rb +141 -75
- data/lib/pwn/sdr/decoder/dect.rb +89 -63
- data/lib/pwn/sdr/decoder/dsp.rb +443 -0
- data/lib/pwn/sdr/decoder/gps.rb +93 -73
- data/lib/pwn/sdr/decoder/gsm.rb +167 -63
- data/lib/pwn/sdr/decoder/lora.rb +118 -65
- data/lib/pwn/sdr/decoder/lte.rb +188 -65
- data/lib/pwn/sdr/decoder/p25.rb +83 -67
- data/lib/pwn/sdr/decoder/zigbee.rb +140 -67
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn.rb +1 -0
- data/spec/lib/pwn/memory_index_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +65 -8
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 44fdb9e4897d83ad9ab54d65bc774eed6be14c379eea42b1a7984abab23c03bd
|
|
4
|
+
data.tar.gz: ba801dabf738c43b3fe7ce9cf40bdfb38d1cf62f7a0b98b05861626c33d2d008
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 205805e3088246b711249ba3b778e9418fb1616ad576dde6c22c0c632409de88b2545fda88ee30316fe7a203a2ff9a46236136e94ba291a7dc27c800d7b8c02c
|
|
7
|
+
data.tar.gz: 4e439c01661f7707c9bf79e8e4be656e76f9d57d5bb17cf9778536bd1a8ebbc63aebd4effbcce38d4ed89f4cfdbe188072ab710a7374d0a951fd8a0c7bd7d0fd
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-
|
|
3
|
+
# on 2026-07-10 16:56:14 UTC using RuboCop version 1.88.2.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -13,7 +13,7 @@ Layout/TrailingWhitespace:
|
|
|
13
13
|
Exclude:
|
|
14
14
|
- 'lib/pwn/banner/radare2_ai.rb'
|
|
15
15
|
|
|
16
|
-
# Offense count:
|
|
16
|
+
# Offense count: 14
|
|
17
17
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
18
18
|
Lint/NestedMethodDefinition:
|
|
19
19
|
Exclude:
|
|
@@ -30,17 +30,16 @@ Lint/RedundantTypeConversion:
|
|
|
30
30
|
- 'lib/pwn/plugins/jenkins.rb'
|
|
31
31
|
- 'lib/pwn/plugins/repl.rb'
|
|
32
32
|
|
|
33
|
-
# Offense count:
|
|
34
|
-
Lint/ShadowedException:
|
|
35
|
-
Exclude:
|
|
36
|
-
- 'lib/pwn/sdr/decoder/flex.rb'
|
|
37
|
-
- 'lib/pwn/sdr/decoder/pocsag.rb'
|
|
38
|
-
|
|
39
|
-
# Offense count: 302
|
|
33
|
+
# Offense count: 296
|
|
40
34
|
# This cop supports safe autocorrection (--autocorrect).
|
|
41
35
|
Lint/UselessAssignment:
|
|
42
36
|
Enabled: false
|
|
43
37
|
|
|
38
|
+
# Offense count: 2
|
|
39
|
+
Lint/UselessConstantScoping:
|
|
40
|
+
Exclude:
|
|
41
|
+
- 'lib/pwn/sdr/decoder/bluetooth.rb'
|
|
42
|
+
|
|
44
43
|
# Offense count: 1
|
|
45
44
|
# This cop supports safe autocorrection (--autocorrect).
|
|
46
45
|
# Configuration parameters: CheckForMethodsWithNoSideEffects.
|
|
@@ -49,7 +48,7 @@ Lint/Void:
|
|
|
49
48
|
- 'bin/pwn_web_cache_deception'
|
|
50
49
|
|
|
51
50
|
# Offense count: 1
|
|
52
|
-
# Configuration parameters:
|
|
51
|
+
# Configuration parameters: Max.
|
|
53
52
|
Metrics/CollectionLiteralLength:
|
|
54
53
|
Exclude:
|
|
55
54
|
- 'lib/pwn/plugins/uri_scheme.rb'
|
|
@@ -66,7 +65,7 @@ Metrics/MethodLength:
|
|
|
66
65
|
Exclude:
|
|
67
66
|
- 'lib/pwn/banner/code_cave.rb'
|
|
68
67
|
|
|
69
|
-
# Offense count:
|
|
68
|
+
# Offense count: 9
|
|
70
69
|
Naming/AccessorMethodName:
|
|
71
70
|
Exclude:
|
|
72
71
|
- 'lib/pwn/ai/anthropic.rb'
|
|
@@ -78,6 +77,13 @@ Naming/AccessorMethodName:
|
|
|
78
77
|
- 'lib/pwn/plugins/jira_data_center.rb'
|
|
79
78
|
- 'lib/pwn/plugins/vin.rb'
|
|
80
79
|
|
|
80
|
+
# Offense count: 5
|
|
81
|
+
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
|
82
|
+
# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
|
|
83
|
+
Naming/MethodParameterName:
|
|
84
|
+
Exclude:
|
|
85
|
+
- 'lib/pwn/sdr/decoder/lora.rb'
|
|
86
|
+
|
|
81
87
|
# Offense count: 2
|
|
82
88
|
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
|
83
89
|
# SupportedStyles: snake_case, normalcase, non_integer
|
|
@@ -98,7 +104,7 @@ Style/Alias:
|
|
|
98
104
|
Style/ClassVars:
|
|
99
105
|
Enabled: false
|
|
100
106
|
|
|
101
|
-
# Offense count:
|
|
107
|
+
# Offense count: 277
|
|
102
108
|
# This cop supports safe autocorrection (--autocorrect).
|
|
103
109
|
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
|
|
104
110
|
# SupportedStyles: assign_to_condition, assign_inside_condition
|
|
@@ -150,13 +156,11 @@ Style/MultipleComparison:
|
|
|
150
156
|
- 'lib/pwn/www/duckduckgo.rb'
|
|
151
157
|
- 'lib/pwn/www/twitter.rb'
|
|
152
158
|
|
|
153
|
-
# Offense count:
|
|
159
|
+
# Offense count: 1
|
|
154
160
|
# This cop supports safe autocorrection (--autocorrect).
|
|
155
161
|
Style/RedundantBegin:
|
|
156
162
|
Exclude:
|
|
157
163
|
- 'lib/pwn/plugins/transparent_browser.rb'
|
|
158
|
-
- 'lib/pwn/sdr/decoder/flex.rb'
|
|
159
|
-
- 'lib/pwn/sdr/decoder/pocsag.rb'
|
|
160
164
|
|
|
161
165
|
# Offense count: 94
|
|
162
166
|
# This cop supports safe autocorrection (--autocorrect).
|
|
@@ -172,13 +176,6 @@ Style/RedundantParentheses:
|
|
|
172
176
|
Exclude:
|
|
173
177
|
- 'lib/pwn/plugins/xxd.rb'
|
|
174
178
|
|
|
175
|
-
# Offense count: 2
|
|
176
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
177
|
-
Style/RedundantSelf:
|
|
178
|
-
Exclude:
|
|
179
|
-
- 'lib/pwn/sdr/decoder/flex.rb'
|
|
180
|
-
- 'lib/pwn/sdr/decoder/pocsag.rb'
|
|
181
|
-
|
|
182
179
|
# Offense count: 12
|
|
183
180
|
# This cop supports safe autocorrection (--autocorrect).
|
|
184
181
|
Style/RedundantStringEscape:
|
|
@@ -189,14 +186,7 @@ Style/RedundantStringEscape:
|
|
|
189
186
|
- 'lib/pwn/sast/redos.rb'
|
|
190
187
|
- 'vagrant/provisioners/kali_customize.rb'
|
|
191
188
|
|
|
192
|
-
# Offense count:
|
|
193
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
194
|
-
Style/RescueModifier:
|
|
195
|
-
Exclude:
|
|
196
|
-
- 'lib/pwn/sdr/decoder/flex.rb'
|
|
197
|
-
- 'lib/pwn/sdr/decoder/pocsag.rb'
|
|
198
|
-
|
|
199
|
-
# Offense count: 19
|
|
189
|
+
# Offense count: 17
|
|
200
190
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
201
191
|
Style/SlicingWithRange:
|
|
202
192
|
Exclude:
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -112,7 +112,7 @@ $ cd /opt/pwn
|
|
|
112
112
|
$ ./install.sh
|
|
113
113
|
$ ./install.sh ruby-gem
|
|
114
114
|
$ pwn
|
|
115
|
-
pwn[v0.5.
|
|
115
|
+
pwn[v0.5.628]:001 >>> PWN.help
|
|
116
116
|
```
|
|
117
117
|
|
|
118
118
|
[](https://youtu.be/G7iLUY4FzsI)
|
|
@@ -136,7 +136,7 @@ $ rvm use ruby-4.0.5@pwn
|
|
|
136
136
|
$ gem uninstall --all --executables pwn
|
|
137
137
|
$ gem install --verbose pwn
|
|
138
138
|
$ pwn
|
|
139
|
-
pwn[v0.5.
|
|
139
|
+
pwn[v0.5.628]:001 >>> PWN.help
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
If using a multi-user RVM install:
|
|
@@ -8,6 +8,22 @@ module PWN
|
|
|
8
8
|
# Tool-call dispatch: takes a single tool_call object (OpenAI shape),
|
|
9
9
|
# looks up the registered handler, parses args, runs it, and returns a
|
|
10
10
|
# JSON string suitable for a role:'tool' message.
|
|
11
|
+
#
|
|
12
|
+
# TOLERANT DISPATCH (local-model scaffolding)
|
|
13
|
+
# -------------------------------------------
|
|
14
|
+
# Local models (qwen/llama/mistral on Ollama) frequently emit almost-
|
|
15
|
+
# right tool calls: `run_shell` instead of `shell`, trailing commas,
|
|
16
|
+
# single-quoted JSON, arguments as a bare string. Strict parsing burns
|
|
17
|
+
# an iteration and often spirals. Dispatch now:
|
|
18
|
+
# * repair_name — Levenshtein-matches unknown names to the closest
|
|
19
|
+
# registered tool and records a Mistakes fingerprint
|
|
20
|
+
# (source: :repair) so the KNOWN MISTAKES block
|
|
21
|
+
# eventually teaches the model the right name.
|
|
22
|
+
# * parse_args — falls back to a JSON5-ish clean-up pass (strip
|
|
23
|
+
# trailing commas, swap single→double quotes, wrap a
|
|
24
|
+
# bare scalar as the tool's sole required arg).
|
|
25
|
+
# Frontier engines never hit these paths — repair is a no-op when the
|
|
26
|
+
# name/JSON are already valid.
|
|
11
27
|
module Dispatch
|
|
12
28
|
# Supported Method Parameters::
|
|
13
29
|
# json_str = PWN::AI::Agent::Dispatch.call(
|
|
@@ -22,10 +38,10 @@ module PWN
|
|
|
22
38
|
name = (fn[:name] || fn['name']).to_s
|
|
23
39
|
raw = fn[:arguments] || fn['arguments'] || '{}'
|
|
24
40
|
|
|
25
|
-
entry = Registry.lookup(name: name)
|
|
41
|
+
entry = Registry.lookup(name: name) || Registry.lookup(name: repair_name(name: name))
|
|
26
42
|
return JSON.generate(error: "unknown tool: #{name}") unless entry
|
|
27
43
|
|
|
28
|
-
args = parse_args(raw: raw)
|
|
44
|
+
args = parse_args(raw: raw, entry: entry)
|
|
29
45
|
result = entry.handler.call(args)
|
|
30
46
|
JSON.generate(success: true, result: result)
|
|
31
47
|
rescue StandardError => e
|
|
@@ -36,15 +52,69 @@ module PWN
|
|
|
36
52
|
)
|
|
37
53
|
end
|
|
38
54
|
|
|
55
|
+
# Supported Method Parameters::
|
|
56
|
+
# fixed = PWN::AI::Agent::Dispatch.repair_name(
|
|
57
|
+
# name: 'required - possibly-wrong tool name emitted by the model'
|
|
58
|
+
# )
|
|
59
|
+
#
|
|
60
|
+
# Returns the closest registered tool name by Levenshtein distance
|
|
61
|
+
# (max distance = 1/3 of the emitted name, min 3) or nil when nothing
|
|
62
|
+
# is close enough. Every successful repair is fingerprinted into
|
|
63
|
+
# Mistakes so the negative-feedback loop trains the model's output
|
|
64
|
+
# format via its own system prompt.
|
|
65
|
+
|
|
66
|
+
public_class_method def self.repair_name(opts = {})
|
|
67
|
+
name = opts[:name].to_s
|
|
68
|
+
return nil if name.empty?
|
|
69
|
+
|
|
70
|
+
pool = Registry.all.map(&:name)
|
|
71
|
+
return nil if pool.empty?
|
|
72
|
+
|
|
73
|
+
best, dist = pool.map { |n| [n, DidYouMean::Levenshtein.distance(n, name)] }
|
|
74
|
+
.min_by(&:last)
|
|
75
|
+
thresh = [(name.length / 3.0).ceil, 3].max
|
|
76
|
+
return nil if dist > thresh
|
|
77
|
+
|
|
78
|
+
if defined?(Mistakes)
|
|
79
|
+
Mistakes.record(
|
|
80
|
+
tool: 'tool_name',
|
|
81
|
+
error: "model emitted '#{name}', repaired to '#{best}'",
|
|
82
|
+
args: name,
|
|
83
|
+
source: :repair
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
best
|
|
87
|
+
rescue StandardError
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
39
91
|
private_class_method def self.parse_args(opts = {})
|
|
40
|
-
raw
|
|
92
|
+
raw = opts[:raw]
|
|
93
|
+
entry = opts[:entry]
|
|
41
94
|
case raw
|
|
42
95
|
when Hash then symbolize(hash: raw)
|
|
43
|
-
when String then raw
|
|
96
|
+
when String then parse_string_args(raw: raw, entry: entry)
|
|
44
97
|
when nil then {}
|
|
45
98
|
else symbolize(hash: raw.to_h)
|
|
46
99
|
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private_class_method def self.parse_string_args(opts = {})
|
|
103
|
+
raw = opts[:raw].to_s
|
|
104
|
+
entry = opts[:entry]
|
|
105
|
+
return {} if raw.strip.empty?
|
|
106
|
+
|
|
107
|
+
JSON.parse(raw, symbolize_names: true)
|
|
47
108
|
rescue JSON::ParserError => e
|
|
109
|
+
# Tolerant retry: strip trailing commas, normalise single quotes,
|
|
110
|
+
# coerce a bare scalar into the tool's sole required parameter.
|
|
111
|
+
cleaned = raw.gsub(/,(\s*[}\]])/, '\1').tr("'", '"')
|
|
112
|
+
begin
|
|
113
|
+
return JSON.parse(cleaned, symbolize_names: true)
|
|
114
|
+
rescue JSON::ParserError
|
|
115
|
+
req = Array(entry&.schema&.dig(:parameters, :required))
|
|
116
|
+
return { req.first.to_sym => raw.strip.gsub(/\A["']|["']\z/, '') } if req.length == 1 && !raw.strip.start_with?('{', '[')
|
|
117
|
+
end
|
|
48
118
|
raise ArgumentError, "invalid JSON arguments: #{e.message}"
|
|
49
119
|
end
|
|
50
120
|
|
|
@@ -72,6 +142,8 @@ module PWN
|
|
|
72
142
|
}
|
|
73
143
|
)
|
|
74
144
|
|
|
145
|
+
PWN::AI::Agent::Dispatch.repair_name(name: 'run_shell') # => 'shell'
|
|
146
|
+
|
|
75
147
|
#{self}.authors
|
|
76
148
|
USAGE
|
|
77
149
|
end
|
|
@@ -23,8 +23,10 @@ module PWN
|
|
|
23
23
|
# Everything is file-backed under ~/.pwn so it survives across REPL
|
|
24
24
|
# restarts and is shared by every future session.
|
|
25
25
|
module Learning
|
|
26
|
-
LEARNING_FILE
|
|
26
|
+
LEARNING_FILE = File.join(Dir.home, '.pwn', 'learning.jsonl')
|
|
27
|
+
FINETUNE_DIR = File.join(Dir.home, '.pwn', 'finetune')
|
|
27
28
|
MAX_MEMORY_ENTRIES = 200
|
|
29
|
+
CLAIM_RX = /CVE-\d{4}-\d{4,7}|\b[A-Za-z][\w.+-]{2,}\s+v?\d+\.\d+(?:\.\d+)?\b/
|
|
28
30
|
|
|
29
31
|
# Supported Method Parameters::
|
|
30
32
|
# entry = PWN::AI::Agent::Learning.note_outcome(
|
|
@@ -127,6 +129,82 @@ module PWN
|
|
|
127
129
|
"#{out}\n"
|
|
128
130
|
end
|
|
129
131
|
|
|
132
|
+
# Supported Method Parameters::
|
|
133
|
+
# msgs = PWN::AI::Agent::Learning.exemplars_for(
|
|
134
|
+
# request: 'required - current user request',
|
|
135
|
+
# limit: 'optional - max exemplar traces to return (default 1)',
|
|
136
|
+
# max_msgs: 'optional - cap on messages per exemplar (default 6)'
|
|
137
|
+
# )
|
|
138
|
+
#
|
|
139
|
+
# Retrieval-augmented BEHAVIOUR: keyword-matches request against
|
|
140
|
+
# prior successful outcomes in learning.jsonl, loads the matching
|
|
141
|
+
# session, and compresses its (user, tool, assistant) trace into a
|
|
142
|
+
# short few-shot exemplar Loop.run splices between system and user.
|
|
143
|
+
# Local models are dramatically better with 1 concrete example than
|
|
144
|
+
# with 25 abstract lessons.
|
|
145
|
+
|
|
146
|
+
public_class_method def self.exemplars_for(opts = {})
|
|
147
|
+
request = opts[:request].to_s.downcase
|
|
148
|
+
limit = (opts[:limit] || 1).to_i
|
|
149
|
+
max_msgs = (opts[:max_msgs] || 6).to_i
|
|
150
|
+
return [] if request.strip.empty?
|
|
151
|
+
|
|
152
|
+
tokens = request.scan(/[a-z0-9_]{3,}/).uniq
|
|
153
|
+
return [] if tokens.empty?
|
|
154
|
+
|
|
155
|
+
hits = outcomes(limit: 500, success: true)
|
|
156
|
+
.reject { |r| r[:session_id].to_s.empty? }
|
|
157
|
+
.map { |r| [r, tokens.count { |t| r[:task].to_s.downcase.include?(t) }] }
|
|
158
|
+
.reject { |_, s| s.zero? }
|
|
159
|
+
.sort_by { |_, s| -s }
|
|
160
|
+
.first(limit)
|
|
161
|
+
.map(&:first)
|
|
162
|
+
|
|
163
|
+
hits.flat_map { |r| compress_exemplar(session_id: r[:session_id], max_msgs: max_msgs) }
|
|
164
|
+
rescue StandardError
|
|
165
|
+
[]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Supported Method Parameters::
|
|
169
|
+
# info = PWN::AI::Agent::Learning.export_finetune(
|
|
170
|
+
# format: 'optional - :sharegpt (default) | :openai_jsonl',
|
|
171
|
+
# out: 'optional - output path (default ~/.pwn/finetune/pwn-YYYYMMDD.jsonl)',
|
|
172
|
+
# min_tools: 'optional - only sessions with >= N tool messages (default 1)'
|
|
173
|
+
# )
|
|
174
|
+
#
|
|
175
|
+
# Turns the learning corpus into a supervised dataset: every session
|
|
176
|
+
# whose learning.jsonl outcome is success:true becomes one training
|
|
177
|
+
# sample (system, user, assistant/tool_calls, tool, ..., final). Pair
|
|
178
|
+
# with a weekly PWN::Cron job that runs `ollama create qwen-pwn -f
|
|
179
|
+
# Modelfile` over the export - the only path to ACTUAL parity with a
|
|
180
|
+
# frontier model, because it changes the weights not just the scaffold.
|
|
181
|
+
|
|
182
|
+
public_class_method def self.export_finetune(opts = {})
|
|
183
|
+
fmt = (opts[:format] || :sharegpt).to_sym
|
|
184
|
+
min_tools = (opts[:min_tools] || 1).to_i
|
|
185
|
+
FileUtils.mkdir_p(FINETUNE_DIR)
|
|
186
|
+
out = opts[:out] || File.join(FINETUNE_DIR, "pwn-#{Time.now.utc.strftime('%Y%m%d')}.jsonl")
|
|
187
|
+
|
|
188
|
+
sids = outcomes(limit: 10_000, success: true).map { |r| r[:session_id] }.compact.uniq
|
|
189
|
+
rows = 0
|
|
190
|
+
File.open(out, 'w') do |f|
|
|
191
|
+
sids.each do |sid|
|
|
192
|
+
t = PWN::Sessions.load(session_id: sid)
|
|
193
|
+
next if t.count { |e| e[:role].to_s == 'tool' } < min_tools
|
|
194
|
+
|
|
195
|
+
conv = t.map { |e| { role: e[:role].to_s, content: e[:content].to_s } }
|
|
196
|
+
.reject { |e| e[:role] == 'system' && e[:content].start_with?('Session started') }
|
|
197
|
+
line = case fmt
|
|
198
|
+
when :openai_jsonl then { messages: conv }
|
|
199
|
+
else { conversations: conv.map { |m| { from: sharegpt_role(role: m[:role]), value: m[:content] } } }
|
|
200
|
+
end
|
|
201
|
+
f.puts(JSON.generate(line))
|
|
202
|
+
rows += 1
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
{ path: out, format: fmt, sessions: sids.length, samples: rows, bytes: File.size(out) }
|
|
206
|
+
end
|
|
207
|
+
|
|
130
208
|
# Supported Method Parameters::
|
|
131
209
|
# skill = PWN::AI::Agent::Learning.distill_skill(
|
|
132
210
|
# name: 'required - snake_case name for the new skill',
|
|
@@ -216,6 +294,7 @@ module PWN
|
|
|
216
294
|
tags: %w[auto loop]
|
|
217
295
|
)
|
|
218
296
|
reflect(session_id: session_id)
|
|
297
|
+
fact_check_local_final(final: opts[:final])
|
|
219
298
|
Extrospection.auto_extrospect(session_id: session_id) if defined?(Extrospection)
|
|
220
299
|
rescue StandardError => e
|
|
221
300
|
warn "[pwn-ai/learning] auto_introspect swallowed: #{e.class}: #{e.message}"
|
|
@@ -382,6 +461,48 @@ module PWN
|
|
|
382
461
|
lessons.uniq.first(5)
|
|
383
462
|
end
|
|
384
463
|
|
|
464
|
+
# Auto fact-check post-filter: local models hallucinate CVEs /
|
|
465
|
+
# versions ~5-10x more than frontier ones. When the active engine is
|
|
466
|
+
# :ollama, scan the final for CVE / version-shaped claims and hand
|
|
467
|
+
# each to Extrospection.verify - refuted claims become
|
|
468
|
+
# Mistakes(tool:'assumption') so KNOWN MISTAKES warns every future
|
|
469
|
+
# run off that specific hallucination.
|
|
470
|
+
private_class_method def self.fact_check_local_final(opts = {})
|
|
471
|
+
return unless defined?(PWN::Env) && PWN::Env.dig(:ai, :active).to_s.downcase.to_sym == :ollama
|
|
472
|
+
return unless defined?(Extrospection) && Extrospection.respond_to?(:verify)
|
|
473
|
+
|
|
474
|
+
claims = opts[:final].to_s.scan(CLAIM_RX).flatten.compact.uniq.first(3)
|
|
475
|
+
claims.each { |c| Extrospection.verify(claim: c, commit: true) }
|
|
476
|
+
rescue StandardError => e
|
|
477
|
+
warn "[pwn-ai/learning] fact_check swallowed: #{e.class}: #{e.message}"
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
private_class_method def self.compress_exemplar(opts = {})
|
|
481
|
+
sid = opts[:session_id]
|
|
482
|
+
cap = opts[:max_msgs] || 6
|
|
483
|
+
t = PWN::Sessions.load(session_id: sid)
|
|
484
|
+
user = t.find { |e| e[:role].to_s == 'user' }
|
|
485
|
+
fin = t.rfind { |e| e[:role].to_s == 'assistant' }
|
|
486
|
+
return [] unless user && fin
|
|
487
|
+
|
|
488
|
+
tools = t.select { |e| e[:role].to_s == 'tool' }.first([cap - 2, 0].max)
|
|
489
|
+
msgs = [{ role: 'user', content: "[exemplar] #{user[:content].to_s[0, 400]}" }]
|
|
490
|
+
tools.each { |e| msgs << { role: 'assistant', content: "[exemplar tool] #{e[:content].to_s[0, 300]}" } }
|
|
491
|
+
msgs << { role: 'assistant', content: "[exemplar final] #{fin[:content].to_s[0, 400]}" }
|
|
492
|
+
msgs
|
|
493
|
+
rescue StandardError
|
|
494
|
+
[]
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
private_class_method def self.sharegpt_role(opts = {})
|
|
498
|
+
case opts[:role].to_s
|
|
499
|
+
when 'user' then 'human'
|
|
500
|
+
when 'assistant' then 'gpt'
|
|
501
|
+
when 'tool' then 'observation'
|
|
502
|
+
else 'system'
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
385
506
|
private_class_method def self.build_skill_from_session(opts = {})
|
|
386
507
|
session_id = opts[:session_id]
|
|
387
508
|
name = opts[:name]
|
|
@@ -422,6 +543,8 @@ module PWN
|
|
|
422
543
|
PWN::AI::Agent::Learning.reflect(session_id: sid) # LLM or heuristic → PWN::Memory
|
|
423
544
|
PWN::AI::Agent::Learning.auto_introspect(session_id: sid, request: req, final: text)
|
|
424
545
|
PWN::AI::Agent::Learning.distill_skill(name: 'quick_recon', session_id: sid)
|
|
546
|
+
PWN::AI::Agent::Learning.exemplars_for(request: 'nmap sweep 10/8') # few-shot for Loop.run
|
|
547
|
+
PWN::AI::Agent::Learning.export_finetune(format: :sharegpt) # -> ~/.pwn/finetune/*.jsonl
|
|
425
548
|
PWN::AI::Agent::Learning.consolidate(max_entries: 200) # dedupe + prune Memory
|
|
426
549
|
PWN::AI::Agent::Learning.to_context(limit: 5) # injected by PromptBuilder
|
|
427
550
|
PWN::AI::Agent::Learning.stats
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -32,8 +32,25 @@ module PWN
|
|
|
32
32
|
# have to fail 3× again to re-learn what it already knew.
|
|
33
33
|
# PromptBuilder.mistakes_block re-injects the top open mistakes and
|
|
34
34
|
# top known fixes into the system prompt of every future turn.
|
|
35
|
+
#
|
|
36
|
+
# LOCAL-MODEL SCAFFOLDING
|
|
37
|
+
# -----------------------
|
|
38
|
+
# When the active engine is :ollama (or the corresponding :agent flags
|
|
39
|
+
# are set) Loop.run additionally:
|
|
40
|
+
# * threads request → PromptBuilder for relevance-ranked MEMORY,
|
|
41
|
+
# * threads request → Registry.definitions(relevance:) for a slimmed
|
|
42
|
+
# tool set (:tool_router),
|
|
43
|
+
# * splices Learning.exemplars_for(request:) between system and user
|
|
44
|
+
# as few-shot behaviour retrieval,
|
|
45
|
+
# * runs a plan-then-act pre-pass (:plan_first) so the model
|
|
46
|
+
# externalises a tool plan before its first dispatch,
|
|
47
|
+
# * escalates to a frontier persona for a 3-line corrective hint
|
|
48
|
+
# once ≥ ESCALATE_AFTER_FAILS in-turn failures accumulate
|
|
49
|
+
# (:escalation_persona) — the local model still produces the final
|
|
50
|
+
# answer so Learning/Metrics stay attributed to :ollama.
|
|
35
51
|
module Loop
|
|
36
|
-
DEFAULT_MAX_ITERS
|
|
52
|
+
DEFAULT_MAX_ITERS = 777
|
|
53
|
+
ESCALATE_AFTER_FAILS = 4
|
|
37
54
|
|
|
38
55
|
ENGINE_MODS = {
|
|
39
56
|
openai: 'PWN::AI::OpenAI',
|
|
@@ -67,6 +84,21 @@ module PWN
|
|
|
67
84
|
DEFAULT_MAX_ITERS
|
|
68
85
|
end
|
|
69
86
|
|
|
87
|
+
private_class_method def self.active_engine
|
|
88
|
+
e = (PWN::Env.dig(:ai, :active) if defined?(PWN::Env)).to_s.downcase.to_sym
|
|
89
|
+
e == :'' ? :openai : e
|
|
90
|
+
rescue StandardError
|
|
91
|
+
:openai
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private_class_method def self.agent_flag(opts = {})
|
|
95
|
+
key = opts[:key]
|
|
96
|
+
v = (PWN::Env.dig(:ai, :agent, key) if defined?(PWN::Env))
|
|
97
|
+
v.nil? ? opts[:default] : v
|
|
98
|
+
rescue StandardError
|
|
99
|
+
opts[:default]
|
|
100
|
+
end
|
|
101
|
+
|
|
70
102
|
# Record per-tool telemetry AND, when the dispatch failed, fingerprint
|
|
71
103
|
# the failure into PWN::AI::Agent::Mistakes so recurring errors are
|
|
72
104
|
# counted, surfaced in the system prompt, and can be resolved with an
|
|
@@ -81,7 +113,7 @@ module PWN
|
|
|
81
113
|
err = raw[/"error":"([^"]{1,300})"/, 1]
|
|
82
114
|
err ||= raw[/"stderr":"([^"]{4,300})"/, 1] unless ok
|
|
83
115
|
dur = started ? (Time.now - started) : 0.0
|
|
84
|
-
Metrics.record(name: name, success: ok, duration: dur, error: err) if defined?(Metrics)
|
|
116
|
+
Metrics.record(name: name, success: ok, duration: dur, error: err, engine: opts[:engine]) if defined?(Metrics)
|
|
85
117
|
m = nil
|
|
86
118
|
m = Mistakes.record(tool: name, error: err || raw[0, 300], args: opts[:args], session_id: opts[:session_id], source: :tool) if !ok && defined?(Mistakes)
|
|
87
119
|
{ ok: ok, err: err, mistake: m }
|
|
@@ -136,6 +168,50 @@ module PWN
|
|
|
136
168
|
"#{guard}\n#{result}"
|
|
137
169
|
end
|
|
138
170
|
|
|
171
|
+
# Plan-then-act pre-pass: force the (usually local) model to
|
|
172
|
+
# externalise a numbered tool plan BEFORE it may call any tool. The
|
|
173
|
+
# plan rides along as an assistant message so every subsequent
|
|
174
|
+
# iteration attends over it — cheap chain-of-thought scaffolding
|
|
175
|
+
# without leaking to the user.
|
|
176
|
+
private_class_method def self.plan_first(opts = {})
|
|
177
|
+
messages = opts[:messages]
|
|
178
|
+
plan_msg = call_engine(
|
|
179
|
+
messages: messages + [{ role: 'user',
|
|
180
|
+
content: 'Before acting, list the exact tool calls (name + key args) you will make, in order. Reply ONLY with the numbered plan — do NOT call any tool yet.' }],
|
|
181
|
+
tools: nil
|
|
182
|
+
)
|
|
183
|
+
return unless plan_msg && !plan_msg[:content].to_s.strip.empty?
|
|
184
|
+
|
|
185
|
+
messages << { role: 'assistant', content: "PLAN:\n#{plan_msg[:content].to_s.strip}" }
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
warn "[pwn-ai/loop] plan_first swallowed: #{e.class}: #{e.message}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Swarm escalation circuit-breaker: when a local model has burned
|
|
191
|
+
# ≥ ESCALATE_AFTER_FAILS distinct failures this turn, ask a frontier
|
|
192
|
+
# persona for a 3-line corrective hint and inject it as a synthetic
|
|
193
|
+
# tool result. Every escalation is recorded as a Mistake so
|
|
194
|
+
# export_finetune can later teach the LoRA to NOT need it.
|
|
195
|
+
private_class_method def self.escalate(opts = {})
|
|
196
|
+
request = opts[:request]
|
|
197
|
+
turn_fails = opts[:turn_fails]
|
|
198
|
+
persona = agent_flag(key: :escalation_persona)
|
|
199
|
+
return nil unless persona && defined?(Swarm) && Swarm.personas.key?(persona.to_sym)
|
|
200
|
+
|
|
201
|
+
summary = turn_fails.map { |k, v| "#{k}: #{v}×" }.join(', ')
|
|
202
|
+
hint = Swarm.ask(
|
|
203
|
+
name: persona.to_s,
|
|
204
|
+
request: "Local agent is stuck on: #{request}\nFailed attempts: #{summary}\n" \
|
|
205
|
+
'Give a 3-line corrective hint (which tool, which args, why). Reply with the hint ONLY.'
|
|
206
|
+
)
|
|
207
|
+
reply = hint.is_a?(Hash) ? hint[:reply].to_s : hint.to_s
|
|
208
|
+
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)
|
|
209
|
+
reply.strip.empty? ? nil : "[pwn-ai/escalation] frontier hint (#{persona}):\n#{reply.strip}"
|
|
210
|
+
rescue StandardError => e
|
|
211
|
+
warn "[pwn-ai/loop] escalate swallowed: #{e.class}: #{e.message}"
|
|
212
|
+
nil
|
|
213
|
+
end
|
|
214
|
+
|
|
139
215
|
# Supported Method Parameters::
|
|
140
216
|
# msg = PWN::AI::Agent::Loop.normalize_llm(
|
|
141
217
|
# response: 'required - chat_with_tools response Hash from any provider'
|
|
@@ -184,9 +260,7 @@ module PWN
|
|
|
184
260
|
messages = opts[:messages]
|
|
185
261
|
tools = opts[:tools]
|
|
186
262
|
|
|
187
|
-
engine =
|
|
188
|
-
engine = :openai if engine == :''
|
|
189
|
-
|
|
263
|
+
engine = active_engine
|
|
190
264
|
mod_name = ENGINE_MODS[engine]
|
|
191
265
|
raise "ERROR: Unsupported AI engine for agent loop: #{engine}" unless mod_name
|
|
192
266
|
|
|
@@ -216,20 +290,24 @@ module PWN
|
|
|
216
290
|
request = opts[:request].to_s
|
|
217
291
|
session_id = opts[:session_id]
|
|
218
292
|
on_tool = opts[:on_tool]
|
|
219
|
-
|
|
293
|
+
engine = active_engine
|
|
294
|
+
local = engine == :ollama
|
|
295
|
+
system_role_content = opts[:system_role_content] ||= PWN::AI::Agent::PromptBuilder.build(session_id: session_id, request: request)
|
|
220
296
|
|
|
221
297
|
Registry.discover
|
|
222
298
|
expose_current_session(session_id: session_id)
|
|
223
299
|
Mistakes.check_user_correction(request: request, session_id: session_id) if defined?(Mistakes)
|
|
224
300
|
|
|
225
|
-
tools = Registry.definitions(enabled: opts[:enabled_toolsets])
|
|
226
|
-
messages = [
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
]
|
|
301
|
+
tools = Registry.definitions(enabled: opts[:enabled_toolsets], relevance: request)
|
|
302
|
+
messages = [{ role: 'system', content: system_role_content }]
|
|
303
|
+
messages.concat(Learning.exemplars_for(request: request)) if local && defined?(Learning) && Learning.respond_to?(:exemplars_for)
|
|
304
|
+
messages << { role: 'user', content: request }
|
|
230
305
|
append_session(session_id: session_id, role: 'user', content: request)
|
|
231
306
|
|
|
307
|
+
plan_first(messages: messages) if agent_flag(key: :plan_first, default: local) && !Array(tools).empty?
|
|
308
|
+
|
|
232
309
|
turn_fails = Hash.new(0)
|
|
310
|
+
escalated = false
|
|
233
311
|
|
|
234
312
|
max_iters.times do |i|
|
|
235
313
|
msg = call_engine(messages: messages, tools: tools)
|
|
@@ -251,7 +329,7 @@ module PWN
|
|
|
251
329
|
entry = Registry.lookup(name: name)
|
|
252
330
|
started = Time.now
|
|
253
331
|
raw = Dispatch.call(tool_call: tc)
|
|
254
|
-
tele = record_metrics(name: name, started: started, raw: raw, args: args, session_id: session_id)
|
|
332
|
+
tele = record_metrics(name: name, started: started, raw: raw, args: args, session_id: session_id, engine: engine)
|
|
255
333
|
result = Result.condition(content: raw, entry: entry)
|
|
256
334
|
|
|
257
335
|
unless tele[:ok]
|
|
@@ -281,6 +359,15 @@ module PWN
|
|
|
281
359
|
content: "#{name} → #{result[0, 1_024]}"
|
|
282
360
|
)
|
|
283
361
|
end
|
|
362
|
+
|
|
363
|
+
next unless local && !escalated && turn_fails.values.sum >= ESCALATE_AFTER_FAILS
|
|
364
|
+
|
|
365
|
+
hint = escalate(request: request, turn_fails: turn_fails, session_id: session_id)
|
|
366
|
+
if hint
|
|
367
|
+
messages << { role: 'tool', tool_call_id: "escalation_#{i}", name: 'frontier_hint', content: hint }
|
|
368
|
+
append_session(session_id: session_id, role: 'tool', content: "frontier_hint → #{hint[0, 1_024]}")
|
|
369
|
+
end
|
|
370
|
+
escalated = true
|
|
284
371
|
end
|
|
285
372
|
|
|
286
373
|
Mistakes.record(tool: 'agent_loop', error: 'iteration budget exhausted without a final answer', session_id: session_id, source: :loop) if defined?(Mistakes)
|
|
@@ -309,6 +396,11 @@ module PWN
|
|
|
309
396
|
Supported engines: #{ENGINE_MODS.keys.join(', ')}
|
|
310
397
|
Set PWN::Env[:ai][:active] to choose; PWN::Env[:ai][:agent][:max_iters] to bound.
|
|
311
398
|
|
|
399
|
+
Local-model scaffolding (PWN::Env[:ai][:agent]):
|
|
400
|
+
:plan_first - Boolean, plan-then-act pre-pass (default: engine == :ollama)
|
|
401
|
+
:tool_router - Boolean, slim Registry.definitions to CORE + top-K relevant
|
|
402
|
+
:escalation_persona - Swarm persona name for frontier corrective hints when stuck
|
|
403
|
+
|
|
312
404
|
#{self}.authors
|
|
313
405
|
USAGE
|
|
314
406
|
end
|