pwn 0.5.696 → 0.5.698
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +5 -0
- data/etc/default_skills/pwn/ai/http_retry/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/repl/SKILL.md +7 -0
- data/lib/pwn/ai/agent/loop.rb +51 -2
- data/lib/pwn/ai/agent/prompt_builder.rb +4 -3
- data/lib/pwn/ai/agent/task_summarizer.rb +2 -2
- data/lib/pwn/ai/agent/tool_guard.rb +129 -33
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +9 -11
- data/lib/pwn/ai/agent/tools/shell.rb +6 -3
- data/lib/pwn/ai/http_retry.rb +9 -0
- data/lib/pwn/ai/ollama.rb +15 -0
- data/lib/pwn/plugins/nmap_it.rb +47 -4
- data/lib/pwn/plugins/repl.rb +186 -3
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +11 -3
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +80 -19
- data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +2 -2
- data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +3 -3
- data/spec/lib/pwn/ai/http_retry_spec.rb +8 -0
- data/spec/lib/pwn/plugins/nmap_it_spec.rb +7 -0
- data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +10 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +37 -1
- data/third_party/pwn_rdoc.jsonl +25 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c333795b07ea88fb7ea338e6b01faf768c62bd07757328fc1698972d43a6c5e8
|
|
4
|
+
data.tar.gz: 0cfbe454002f7ffc79571162a96367aaf3d69017d997cb024c4a475ac3bbab7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a699a8308cb00f04bdf85869647ec381009daf04fe4d8c972c62d5ed57b819b4b719672fe4ad64966c316520a2496bdeb4914c002f526e886b877f7b5d11928
|
|
7
|
+
data.tar.gz: 5b2d1fa1f979bc4852304ed09b7122817a71689114f4c4f6a1a46798b01e9d3bb186b8af28aa8d08288c470b01d323735fb252ccc9c45426835d8b717ca8e382
|
|
@@ -42,6 +42,11 @@ PWN::AI::Agent::ToolGuard.present(opts)
|
|
|
42
42
|
- `invalid_payload`
|
|
43
43
|
- `host_load`
|
|
44
44
|
- `deadline_s`
|
|
45
|
+
- `reset_timeout_budget`
|
|
46
|
+
- `mutation_count`
|
|
47
|
+
- `payload_spent`
|
|
48
|
+
- `note_timeout`
|
|
49
|
+
- `next_timeout`
|
|
45
50
|
- `timeout_lesson`
|
|
46
51
|
- `timeout_result`
|
|
47
52
|
- `timeout_prior_count`
|
|
@@ -46,6 +46,13 @@ PWN::Plugins::REPL.ready_tty(opts)
|
|
|
46
46
|
- `install_pwn_ai_completer`
|
|
47
47
|
- `restore_pwn_ai_completer`
|
|
48
48
|
- `pwn_ai_dispatch_slash`
|
|
49
|
+
- `pwn_ai_engines`
|
|
50
|
+
- `pwn_ai_provider_class`
|
|
51
|
+
- `pwn_ai_model_ids`
|
|
52
|
+
- `pwn_ai_list_llms`
|
|
53
|
+
- `pwn_ai_engine_model`
|
|
54
|
+
- `pwn_ai_run_model`
|
|
55
|
+
- `persist_ai_selection`
|
|
49
56
|
- `pwn_ai_run_cron`
|
|
50
57
|
- `pwn_ai_run_sessions`
|
|
51
58
|
- `pwn_ai_run_memory`
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -822,7 +822,8 @@ module PWN
|
|
|
822
822
|
lesson = ToolGuard.timeout_lesson(
|
|
823
823
|
tool: name,
|
|
824
824
|
payload: opts[:args].to_s,
|
|
825
|
-
timeout: err.to_s[/timeout after (\d+)/, 1].to_i
|
|
825
|
+
timeout: err.to_s[/timeout after (\d+)/, 1].to_i,
|
|
826
|
+
task: timeout_task(opts)
|
|
826
827
|
)
|
|
827
828
|
err = lesson[:error] if lesson[:error].to_s.strip.length.positive?
|
|
828
829
|
shape = :timeout
|
|
@@ -835,6 +836,32 @@ module PWN
|
|
|
835
836
|
{ ok: true, err: nil, mistake: nil }
|
|
836
837
|
end
|
|
837
838
|
|
|
839
|
+
private_class_method def self.timeout_task(opts = {})
|
|
840
|
+
st = opts[:ts_state]
|
|
841
|
+
if st.respond_to?(:[])
|
|
842
|
+
idx = st[:plan_idx] || st['plan_idx']
|
|
843
|
+
return "task-#{idx}" unless idx.nil?
|
|
844
|
+
end
|
|
845
|
+
if st.respond_to?(:plan_idx)
|
|
846
|
+
idx = st.plan_idx
|
|
847
|
+
return "task-#{idx}" unless idx.nil?
|
|
848
|
+
end
|
|
849
|
+
opts[:session_id].to_s
|
|
850
|
+
rescue StandardError
|
|
851
|
+
opts[:session_id].to_s
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
private_class_method def self.engine_transient?(opts = {})
|
|
855
|
+
err = opts[:error]
|
|
856
|
+
return false unless err
|
|
857
|
+
|
|
858
|
+
return PWN::AI::HttpRetry.retryable?(error: err) if defined?(PWN::AI::HttpRetry) && PWN::AI::HttpRetry.respond_to?(:retryable?)
|
|
859
|
+
|
|
860
|
+
err.message.to_s.match?(/HTTP 50[234]|Gateway Time-out|stream absolute timeout/i)
|
|
861
|
+
rescue StandardError
|
|
862
|
+
false
|
|
863
|
+
end
|
|
864
|
+
|
|
838
865
|
# E1 — did the environment change under this tool? If Metrics CUSUM
|
|
839
866
|
# tripped for it in the last hour AND Extrospection.drift shows a
|
|
840
867
|
# toolchain/net/repo change, blame the WORLD not the AGENT.
|
|
@@ -2271,6 +2298,7 @@ module PWN
|
|
|
2271
2298
|
start_debug_session(opts)
|
|
2272
2299
|
loud_debug_tui!(debug: opts[:debug])
|
|
2273
2300
|
debug_progress(msg: "Loop.run start request=#{request[0, 240]}", debug: opts[:debug])
|
|
2301
|
+
ToolGuard.reset_timeout_budget! if defined?(ToolGuard) && ToolGuard.respond_to?(:reset_timeout_budget!)
|
|
2274
2302
|
nested = defined?(TurnFinalizer) && TurnFinalizer.user_path?
|
|
2275
2303
|
TurnFinalizer.enter_user_path! if defined?(TurnFinalizer)
|
|
2276
2304
|
engine = active_engine
|
|
@@ -2456,6 +2484,7 @@ module PWN
|
|
|
2456
2484
|
|
|
2457
2485
|
turn_fails = Hash.new(0)
|
|
2458
2486
|
escalated = false
|
|
2487
|
+
engine_blips = 0
|
|
2459
2488
|
maybe_park_budget_scars!
|
|
2460
2489
|
maybe_extinguish_parked!
|
|
2461
2490
|
|
|
@@ -2469,7 +2498,27 @@ module PWN
|
|
|
2469
2498
|
inject_task_focus!(messages: messages, state: ts_state, request: request) unless skip_compass
|
|
2470
2499
|
|
|
2471
2500
|
t0 = Time.now
|
|
2472
|
-
|
|
2501
|
+
begin
|
|
2502
|
+
msg = call_engine(messages: messages, tools: tools, ts_state: ts_state)
|
|
2503
|
+
rescue StandardError => e
|
|
2504
|
+
if engine_transient?(error: e)
|
|
2505
|
+
engine_blips += 1
|
|
2506
|
+
debug_progress(msg: "engine hop failed #{e.class}: #{e.message.to_s[0, 240]} blip=#{engine_blips}")
|
|
2507
|
+
if engine_blips >= 5
|
|
2508
|
+
txt = "[pwn-ai] engine hop failed after #{engine_blips} tries: #{e.message.to_s[0, 240]}"
|
|
2509
|
+
debug_final_text!(text: txt)
|
|
2510
|
+
final_chars = txt.length
|
|
2511
|
+
return txt
|
|
2512
|
+
end
|
|
2513
|
+
messages << {
|
|
2514
|
+
role: 'user',
|
|
2515
|
+
content: '[pwn-ai] engine hop failed (transient). Keep calling CORE_TOOLS. ' \
|
|
2516
|
+
"Do not stop. (#{e.message.to_s[0, 180]})"
|
|
2517
|
+
}
|
|
2518
|
+
next
|
|
2519
|
+
end
|
|
2520
|
+
raise
|
|
2521
|
+
end
|
|
2473
2522
|
engine_s += (Time.now - t0)
|
|
2474
2523
|
PWN::Plugins::TTYSpinner.halt_all! if defined?(PWN::Plugins::TTYSpinner)
|
|
2475
2524
|
if msg.nil?
|
|
@@ -100,9 +100,10 @@ module PWN
|
|
|
100
100
|
|
|
101
101
|
HOST LOAD
|
|
102
102
|
#{host_load_block}
|
|
103
|
-
On pwn_eval/shell timeout:
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
On pwn_eval/shell timeout: keep the same ruby/command and retry
|
|
104
|
+
with timeout += 180 until it finishes or the 3-hour budget is
|
|
105
|
+
gone. Only then rewrite the payload for the same goal (max 10
|
|
106
|
+
mutations per task). Any payload.
|
|
106
107
|
Record a mistake either way.
|
|
107
108
|
|
|
108
109
|
AUTONOMY
|
|
@@ -573,8 +573,8 @@ module PWN
|
|
|
573
573
|
# model cannot stall the user's real answer, and stay quiet on timeout.
|
|
574
574
|
private_class_method def self.sidecar_timeout
|
|
575
575
|
n = (PWN::Env.dig(:ai, :agent, :sidecar_llm_timeout) if defined?(PWN::Env)).to_i
|
|
576
|
-
n =
|
|
577
|
-
n =
|
|
576
|
+
n = 180 if n < 30
|
|
577
|
+
n = 180 if n > 180
|
|
578
578
|
n
|
|
579
579
|
rescue StandardError
|
|
580
580
|
20
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
3
5
|
module PWN
|
|
4
6
|
module AI
|
|
5
7
|
module Agent
|
|
@@ -115,82 +117,176 @@ module PWN
|
|
|
115
117
|
{ ncpu: 1, load1: 0.0, mem_avail_mb: 0 }
|
|
116
118
|
end
|
|
117
119
|
|
|
120
|
+
TIMEOUT_STEP_S = 180
|
|
121
|
+
TIMEOUT_MAX_S = 10_800
|
|
122
|
+
MUTATION_MAX = 10
|
|
123
|
+
|
|
118
124
|
# Conservative wall-clock seconds for shell / pwn_eval.
|
|
119
|
-
# Explicit
|
|
120
|
-
# default from loadavg / ncpu / MemAvailable
|
|
121
|
-
#
|
|
125
|
+
# Explicit timeout is honored for any payload (1..TIMEOUT_MAX_S).
|
|
126
|
+
# Omit → host-derived default from loadavg / ncpu / MemAvailable.
|
|
127
|
+
# No tool-name sniffing: a 65k scan and `ls` use the same math.
|
|
122
128
|
public_class_method def self.deadline_s(opts = {})
|
|
123
129
|
kind = opts[:kind].to_s.to_sym
|
|
124
|
-
max = kind == :shell ? 180 : 90
|
|
125
130
|
asked = opts[:timeout] || opts[:timeout_s]
|
|
126
131
|
asked_i = asked.to_i
|
|
127
|
-
if asked_i.positive?
|
|
128
|
-
lo = 1
|
|
129
|
-
hi = max
|
|
130
|
-
return asked_i.clamp(lo, hi)
|
|
131
|
-
end
|
|
132
|
+
return asked_i.clamp(1, TIMEOUT_MAX_S) if asked_i.positive?
|
|
132
133
|
|
|
133
134
|
snap = host_load
|
|
134
135
|
ncpu = [snap[:ncpu].to_i, 1].max
|
|
135
136
|
load1 = snap[:load1].to_f
|
|
136
137
|
mem = snap[:mem_avail_mb].to_i
|
|
138
|
+
default_max = kind == :shell ? 180 : 90
|
|
137
139
|
base = kind == :shell ? 30 : 20
|
|
138
140
|
base += 15 if load1 > ncpu
|
|
139
141
|
base += 10 if load1 > (ncpu * 1.5)
|
|
140
142
|
base += 10 if mem.positive? && mem < 512
|
|
141
|
-
base.clamp(8,
|
|
143
|
+
base.clamp(8, default_max)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
public_class_method def self.reset_timeout_budget(opts = {})
|
|
147
|
+
return :noop unless opts.is_a?(Hash)
|
|
148
|
+
|
|
149
|
+
@timeout_spent = {}
|
|
150
|
+
@timeout_mutations = {}
|
|
151
|
+
@timeout_mutated = {}
|
|
152
|
+
:reset
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
public_class_method def self.reset_timeout_budget!
|
|
156
|
+
reset_timeout_budget
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
public_class_method def self.mutation_count(opts = {})
|
|
160
|
+
return 0 unless opts.is_a?(Hash)
|
|
161
|
+
|
|
162
|
+
timeout_mutations[task_key(opts)].to_i
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
public_class_method def self.payload_spent(opts = {})
|
|
166
|
+
return 0 unless opts.is_a?(Hash)
|
|
167
|
+
|
|
168
|
+
timeout_spent[payload_key(opts)].to_i
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
public_class_method def self.note_timeout!(opts = {})
|
|
172
|
+
return 0 unless opts.is_a?(Hash)
|
|
173
|
+
|
|
174
|
+
timeout = opts[:timeout].to_i
|
|
175
|
+
timeout = 1 if timeout < 1
|
|
176
|
+
key = payload_key(opts)
|
|
177
|
+
timeout_spent[key] = timeout_spent[key].to_i + timeout
|
|
178
|
+
if budget_exhausted?(opts.merge(spent: timeout_spent[key])) && !timeout_mutated[key]
|
|
179
|
+
timeout_mutated[key] = true
|
|
180
|
+
tkey = task_key(opts)
|
|
181
|
+
timeout_mutations[tkey] = timeout_mutations[tkey].to_i + 1
|
|
182
|
+
end
|
|
183
|
+
timeout_spent[key]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
public_class_method def self.next_timeout(opts = {})
|
|
187
|
+
base = opts[:timeout].to_i
|
|
188
|
+
base = 1 if base < 1
|
|
189
|
+
spent = opts.key?(:spent) ? opts[:spent].to_i : payload_spent(opts)
|
|
190
|
+
remaining = TIMEOUT_MAX_S - spent
|
|
191
|
+
remaining = 0 if remaining.negative?
|
|
192
|
+
[base + TIMEOUT_STEP_S, remaining, TIMEOUT_MAX_S].min
|
|
142
193
|
end
|
|
143
194
|
|
|
144
|
-
#
|
|
145
|
-
# 1.
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
195
|
+
# Timeout policy (loop-law, not a skill):
|
|
196
|
+
# 1. Same payload: timeout += 180 until the 3-hour budget is gone.
|
|
197
|
+
# 2. At the 3-hour cap: rewrite ruby/command for the same goal
|
|
198
|
+
# (one mutation). Max MUTATION_MAX mutations per task.
|
|
199
|
+
# 3. After MUTATION_MAX mutations: stop (exhausted).
|
|
149
200
|
public_class_method def self.timeout_lesson(opts = {})
|
|
150
201
|
return { scenario: :construction, error: '', hint: '' } unless opts.is_a?(Hash)
|
|
151
202
|
|
|
152
203
|
tool = opts[:tool].to_s
|
|
153
204
|
timeout = opts[:timeout].to_i
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
205
|
+
spent = payload_spent(opts)
|
|
206
|
+
spent_after = spent >= timeout && timeout.positive? ? spent : spent + [timeout, 1].max
|
|
207
|
+
nxt = next_timeout(timeout: timeout, spent: spent_after)
|
|
208
|
+
mutations = mutation_count(opts)
|
|
209
|
+
if budget_exhausted?(opts.merge(timeout: timeout, spent: spent_after))
|
|
210
|
+
if mutations >= MUTATION_MAX
|
|
211
|
+
{
|
|
212
|
+
scenario: :exhausted,
|
|
213
|
+
error: "#{tool} timeout: #{MUTATION_MAX} mutations exhausted for this task",
|
|
214
|
+
hint: "This task hit the mutation cap (#{MUTATION_MAX} rewrites after " \
|
|
215
|
+
'3-hour budgets). Do not retry the same payload. Report what ' \
|
|
216
|
+
'was tried and what remains blocked.'
|
|
217
|
+
}
|
|
218
|
+
else
|
|
219
|
+
{
|
|
220
|
+
scenario: :construction,
|
|
221
|
+
error: "#{tool} timeout: 3-hour budget exhausted; reconstruct payload to same goal",
|
|
222
|
+
hint: "The #{tool} payload used its 3-hour budget. Generate different " \
|
|
223
|
+
'ruby/command for the same goal. Mutation ' \
|
|
224
|
+
"#{[mutations, 1].max}/#{MUTATION_MAX}."
|
|
225
|
+
}
|
|
226
|
+
end
|
|
164
227
|
else
|
|
165
228
|
{
|
|
166
|
-
scenario: :
|
|
167
|
-
error: "#{tool} timeout:
|
|
168
|
-
hint: "
|
|
169
|
-
|
|
170
|
-
'Do not first raise timeout. Record this so it does not recur.'
|
|
229
|
+
scenario: :deadline,
|
|
230
|
+
error: "#{tool} timeout: deadline too short; retry with timeout += 180",
|
|
231
|
+
hint: "Keep the same #{tool} payload. This timeout (#{timeout}s) was too " \
|
|
232
|
+
"short. Retry with timeout += 180 (next_timeout=#{nxt})."
|
|
171
233
|
}
|
|
172
234
|
end
|
|
173
235
|
end
|
|
174
236
|
|
|
175
237
|
public_class_method def self.timeout_result(opts = {})
|
|
176
|
-
return { stdout: '', stderr: '', exit: nil, error: 'timeout', scenario: :
|
|
238
|
+
return { stdout: '', stderr: '', exit: nil, error: 'timeout', scenario: :deadline, hint: '', next_timeout: TIMEOUT_STEP_S, shell: shell_name } unless opts.is_a?(Hash)
|
|
177
239
|
|
|
240
|
+
timeout = opts[:timeout].to_i
|
|
241
|
+
note_timeout!(opts)
|
|
178
242
|
lesson = timeout_lesson(
|
|
179
243
|
tool: opts[:tool],
|
|
180
244
|
payload: opts[:payload],
|
|
181
|
-
timeout:
|
|
245
|
+
timeout: timeout,
|
|
246
|
+
task: opts[:task]
|
|
182
247
|
)
|
|
183
248
|
{
|
|
184
249
|
stdout: opts[:stdout].to_s,
|
|
185
250
|
stderr: opts[:stderr].to_s,
|
|
186
251
|
exit: nil,
|
|
187
|
-
error: "timeout after #{
|
|
252
|
+
error: "timeout after #{timeout}s",
|
|
188
253
|
scenario: lesson[:scenario],
|
|
189
254
|
hint: lesson[:hint],
|
|
255
|
+
next_timeout: next_timeout(timeout: timeout, spent: payload_spent(opts)),
|
|
256
|
+
mutations: mutation_count(opts),
|
|
190
257
|
shell: opts[:shell] || shell_name
|
|
191
258
|
}
|
|
192
259
|
end
|
|
193
260
|
|
|
261
|
+
private_class_method def self.timeout_spent
|
|
262
|
+
@timeout_spent ||= {}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
private_class_method def self.timeout_mutations
|
|
266
|
+
@timeout_mutations ||= {}
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
private_class_method def self.timeout_mutated
|
|
270
|
+
@timeout_mutated ||= {}
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
private_class_method def self.task_key(opts = {})
|
|
274
|
+
t = opts[:task]
|
|
275
|
+
t = opts[:session_id] if t.to_s.strip.empty?
|
|
276
|
+
t.to_s.strip.empty? ? 'default' : t.to_s
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
private_class_method def self.payload_key(opts = {})
|
|
280
|
+
payload = opts[:payload].to_s
|
|
281
|
+
"#{task_key(opts)}:#{Digest::SHA256.hexdigest(payload)}"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
private_class_method def self.budget_exhausted?(opts = {})
|
|
285
|
+
spent = opts[:spent]
|
|
286
|
+
spent = payload_spent(opts) if spent.nil?
|
|
287
|
+
opts[:timeout].to_i >= TIMEOUT_MAX_S || spent.to_i >= TIMEOUT_MAX_S
|
|
288
|
+
end
|
|
289
|
+
|
|
194
290
|
public_class_method def self.timeout_prior_count(opts = {})
|
|
195
291
|
return 0 unless opts.is_a?(Hash)
|
|
196
292
|
return 0 unless defined?(PWN::AI::Agent::Mistakes)
|
|
@@ -24,7 +24,10 @@ PWN::AI::Agent::Registry.register(
|
|
|
24
24
|
'browser. Do not open a second browser. Close once with ' \
|
|
25
25
|
'Close once with TransparentBrowser.close(browser_obj: browser_obj). ' \
|
|
26
26
|
'Pass timeout as a conservative integer seconds estimate given HOST ' \
|
|
27
|
-
'LOAD (loadavg, ncpu, mem). Omit for a host-derived default
|
|
27
|
+
'LOAD (loadavg, ncpu, mem). Omit for a host-derived default. ' \
|
|
28
|
+
'Explicit timeout is honored up to 10800s (3 hours) for any payload. On ' \
|
|
29
|
+
'timeout, keep the same payload and retry with timeout += 180 ' \
|
|
30
|
+
'until the 3-hour budget is gone; then rewrite (max 10 mutations/task). ' \
|
|
28
31
|
'Returns captured stdout plus the inspected value of the last expression.',
|
|
29
32
|
parameters: {
|
|
30
33
|
type: 'object',
|
|
@@ -32,7 +35,7 @@ PWN::AI::Agent::Registry.register(
|
|
|
32
35
|
code: { type: 'string', description: 'Ruby source to evaluate.' },
|
|
33
36
|
timeout: {
|
|
34
37
|
type: 'integer',
|
|
35
|
-
description: 'Conservative seconds this eval should take given HOST LOAD. Omit for host-derived default.
|
|
38
|
+
description: 'Conservative seconds this eval should take given HOST LOAD. Omit for a host-derived default. Explicit values honored 1..10800 (3 hours). On timeout keep the same payload and timeout += 180; rewrite only after the 3-hour budget (max 10 mutations/task).'
|
|
36
39
|
}
|
|
37
40
|
},
|
|
38
41
|
required: %w[code]
|
|
@@ -61,7 +64,7 @@ PWN::AI::Agent::Registry.register(
|
|
|
61
64
|
old_stdout = $stdout
|
|
62
65
|
buf = StringIO.new
|
|
63
66
|
$stdout = buf
|
|
64
|
-
timeout = PWN::AI::Agent::ToolGuard.deadline_s(timeout: args[:timeout], kind: :eval)
|
|
67
|
+
timeout = PWN::AI::Agent::ToolGuard.deadline_s(timeout: args[:timeout], kind: :eval, payload: code)
|
|
65
68
|
begin
|
|
66
69
|
# rubocop:disable Security/Eval
|
|
67
70
|
# INTENTIONAL: this IS the pwn-ai → PWN bridge
|
|
@@ -89,17 +92,12 @@ PWN::AI::Agent::Registry.register(
|
|
|
89
92
|
# rubocop:enable Security/Eval
|
|
90
93
|
{ stdout: buf.string, value: val.inspect, timeout: timeout }
|
|
91
94
|
rescue Timeout::Error
|
|
92
|
-
|
|
95
|
+
PWN::AI::Agent::ToolGuard.timeout_result(
|
|
93
96
|
tool: 'pwn_eval',
|
|
94
97
|
payload: code,
|
|
95
|
-
timeout: timeout
|
|
98
|
+
timeout: timeout,
|
|
99
|
+
stdout: buf.string
|
|
96
100
|
)
|
|
97
|
-
{
|
|
98
|
-
stdout: buf.string,
|
|
99
|
-
error: "timeout after #{timeout}s",
|
|
100
|
-
scenario: lesson[:scenario],
|
|
101
|
-
hint: lesson[:hint]
|
|
102
|
-
}
|
|
103
101
|
rescue ScriptError, StandardError => e
|
|
104
102
|
{
|
|
105
103
|
stdout: buf.string,
|
|
@@ -23,14 +23,17 @@ PWN::AI::Agent::Registry.register(
|
|
|
23
23
|
'stdout/stderr/exit code. Use for OS-level work: nmap, curl, ' \
|
|
24
24
|
'ls, git, file inspection, anything not in the PWN:: namespace. ' \
|
|
25
25
|
'Pass timeout as a conservative integer seconds estimate given ' \
|
|
26
|
-
'HOST LOAD (loadavg, ncpu, mem). Omit for a host-derived default
|
|
26
|
+
'HOST LOAD (loadavg, ncpu, mem). Omit for a host-derived default. ' \
|
|
27
|
+
'Explicit timeout is honored up to 10800s (3 hours) for any payload. On ' \
|
|
28
|
+
'timeout, keep the same payload and retry with timeout += 180 ' \
|
|
29
|
+
'until the 3-hour budget is gone; then rewrite (max 10 mutations/task).',
|
|
27
30
|
parameters: {
|
|
28
31
|
type: 'object',
|
|
29
32
|
properties: {
|
|
30
33
|
command: { type: 'string', description: 'The exact shell command to run.' },
|
|
31
34
|
timeout: {
|
|
32
35
|
type: 'integer',
|
|
33
|
-
description: 'Conservative seconds this command should take given HOST LOAD. Omit for host-derived default.
|
|
36
|
+
description: 'Conservative seconds this command should take given HOST LOAD. Omit for a host-derived default. Explicit values honored 1..10800 (3 hours). On timeout keep the same payload and timeout += 180; rewrite only after the 3-hour budget (max 10 mutations/task).'
|
|
34
37
|
}
|
|
35
38
|
},
|
|
36
39
|
required: %w[command]
|
|
@@ -52,7 +55,7 @@ PWN::AI::Agent::Registry.register(
|
|
|
52
55
|
.gsub(/\\\r?\n/, ' ')
|
|
53
56
|
.gsub(/\\+\s*\z/, '')
|
|
54
57
|
.strip
|
|
55
|
-
timeout = PWN::AI::Agent::ToolGuard.deadline_s(timeout: args[:timeout], kind: :shell)
|
|
58
|
+
timeout = PWN::AI::Agent::ToolGuard.deadline_s(timeout: args[:timeout], kind: :shell, payload: cmd)
|
|
56
59
|
if cmd.empty? || PWN::AI::Agent::ToolGuard.placeholder?(text: cmd)
|
|
57
60
|
return PWN::AI::Agent::ToolGuard.invalid_payload(
|
|
58
61
|
hint: 'command is required (string). Do not send ..., {...}, {…}, or empty. ' \
|
data/lib/pwn/ai/http_retry.rb
CHANGED
|
@@ -27,6 +27,15 @@ module PWN
|
|
|
27
27
|
DEFAULT_MAX_ATTEMPTS
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
public_class_method def self.retryable?(opts = {})
|
|
31
|
+
err = opts[:error]
|
|
32
|
+
msg = err.respond_to?(:message) ? err.message.to_s : err.to_s
|
|
33
|
+
msg = opts[:message].to_s if msg.empty?
|
|
34
|
+
msg.match?(/HTTP 50[234]|Gateway Time-out|stream absolute timeout|ReadTimeout|Net::ReadTimeout|Timed out reading/i)
|
|
35
|
+
rescue StandardError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
30
39
|
public_class_method def self.retry_after_s(opts = {})
|
|
31
40
|
retry_count = [opts[:retry_count].to_i, 1].max
|
|
32
41
|
retry_after = 0
|
data/lib/pwn/ai/ollama.rb
CHANGED
|
@@ -198,6 +198,21 @@ module PWN
|
|
|
198
198
|
retry if retry_count < max_attempts
|
|
199
199
|
|
|
200
200
|
nil
|
|
201
|
+
rescue StandardError => e
|
|
202
|
+
if PWN::AI::HttpRetry.retryable?(error: e) && retry_count + 1 < max_attempts
|
|
203
|
+
retry_count += 1
|
|
204
|
+
unless opts[:quiet]
|
|
205
|
+
PWN::AI::HttpRetry.report_event(
|
|
206
|
+
label: 'ollama', which_self: self, quiet: opts[:quiet],
|
|
207
|
+
http_method: http_method, rest_call: rest_call,
|
|
208
|
+
extra: "retryable attempt=#{retry_count}/#{max_attempts}",
|
|
209
|
+
error: e
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
sleep(PWN::AI::HttpRetry.retry_after_s(retry_count: retry_count) + rand(0.3..2.0))
|
|
213
|
+
retry
|
|
214
|
+
end
|
|
215
|
+
raise e
|
|
201
216
|
end
|
|
202
217
|
rescue RestClient::ExceptionWithResponse => e
|
|
203
218
|
body = begin
|
data/lib/pwn/plugins/nmap_it.rb
CHANGED
|
@@ -14,19 +14,53 @@ module PWN
|
|
|
14
14
|
# nmap.connect_scan = true
|
|
15
15
|
# nmap.service_scan = true
|
|
16
16
|
# nmap.verbose = true
|
|
17
|
-
# nmap.ports = [1..1024,1337]
|
|
17
|
+
# nmap.ports = [1..1024, 1337]
|
|
18
18
|
# nmap.targets = '127.0.0.1'
|
|
19
|
-
# nmap.xml = '/tmp/nmap_port_scan_res.xml'
|
|
19
|
+
# nmap.xml = '/tmp/nmap_port_scan_res.xml' # alias of output_xml
|
|
20
20
|
# end
|
|
21
|
+
#
|
|
22
|
+
# PWN::Plugins::NmapIt.port_scan(
|
|
23
|
+
# targets: '127.0.0.1',
|
|
24
|
+
# ports: '1-65535',
|
|
25
|
+
# connect_scan: true,
|
|
26
|
+
# service_scan: true,
|
|
27
|
+
# script: 'vuln,safe',
|
|
28
|
+
# xml: '/tmp/nmap_port_scan_res.xml'
|
|
29
|
+
# )
|
|
21
30
|
|
|
22
|
-
public_class_method def self.port_scan
|
|
31
|
+
public_class_method def self.port_scan(opts = {})
|
|
23
32
|
Nmap::Command.sudo do |nmap|
|
|
24
|
-
|
|
33
|
+
apply_port_scan_opts(nmap: nmap, opts: opts)
|
|
34
|
+
yield(nmap_compat(nmap: nmap)) if block_given?
|
|
25
35
|
end
|
|
26
36
|
rescue StandardError => e
|
|
27
37
|
raise e
|
|
28
38
|
end
|
|
29
39
|
|
|
40
|
+
private_class_method def self.apply_port_scan_opts(opts = {})
|
|
41
|
+
nmap = opts[:nmap]
|
|
42
|
+
args = opts[:opts]
|
|
43
|
+
return nmap unless args.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
nmap.targets = args[:targets] || args[:target] if args[:targets] || args[:target]
|
|
46
|
+
nmap.ports = args[:ports] if args.key?(:ports)
|
|
47
|
+
xml = args[:xml] || args[:output_xml]
|
|
48
|
+
nmap.output_xml = xml if xml
|
|
49
|
+
nmap.connect_scan = args[:connect_scan] if args.key?(:connect_scan)
|
|
50
|
+
nmap.service_scan = args[:service_scan] || args[:service_detection] if args.key?(:service_scan) || args.key?(:service_detection)
|
|
51
|
+
nmap.verbose = args[:verbose] if args.key?(:verbose)
|
|
52
|
+
scripts = args[:script] || args[:scripts] || args[:vuln_scripts]
|
|
53
|
+
nmap.script = scripts if scripts
|
|
54
|
+
nmap
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private_class_method def self.nmap_compat(opts = {})
|
|
58
|
+
nmap = opts[:nmap]
|
|
59
|
+
nmap.define_singleton_method(:xml=) { |path| self.output_xml = path }
|
|
60
|
+
nmap.define_singleton_method(:xml) { output_xml }
|
|
61
|
+
nmap
|
|
62
|
+
end
|
|
63
|
+
|
|
30
64
|
# Supported Method Parameters::
|
|
31
65
|
# PWN::Plugins::NmapIt.parse_xml_results(:xml_file => 'required - path to nmap xml results') do |xml|
|
|
32
66
|
# puts xml.public_methods
|
|
@@ -103,6 +137,15 @@ module PWN
|
|
|
103
137
|
nmap.xml = '/tmp/nmap_port_scan_res.xml'
|
|
104
138
|
end
|
|
105
139
|
|
|
140
|
+
#{self}.port_scan(
|
|
141
|
+
targets: '127.0.0.1',
|
|
142
|
+
ports: '1-65535',
|
|
143
|
+
connect_scan: true,
|
|
144
|
+
service_scan: true,
|
|
145
|
+
script: 'vuln,safe',
|
|
146
|
+
xml: '/tmp/nmap_port_scan_res.xml'
|
|
147
|
+
)
|
|
148
|
+
|
|
106
149
|
#{self}.parse_xml_results(:xml_file => 'required - path to nmap xml results') do |xml|
|
|
107
150
|
xml.each_host do |host|
|
|
108
151
|
puts host.ip
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -1284,8 +1284,9 @@ module PWN
|
|
|
1284
1284
|
raise
|
|
1285
1285
|
rescue StandardError => e
|
|
1286
1286
|
PWN::Plugins::Log.note_exception!(error: e, where: 'native agent loop', which_self: PWN::Plugins::REPL) if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.respond_to?(:note_exception!)
|
|
1287
|
-
warn "[pwn-ai] native agent loop failed (#{e.class}: #{e.message
|
|
1288
|
-
|
|
1287
|
+
warn "[pwn-ai] native agent loop failed (#{e.class}: #{e.message.to_s.split("\n").first})"
|
|
1288
|
+
request.replace('nil')
|
|
1289
|
+
next
|
|
1289
1290
|
ensure
|
|
1290
1291
|
PWN::Plugins::REPL.ready_tty!
|
|
1291
1292
|
end
|
|
@@ -1557,7 +1558,7 @@ module PWN
|
|
|
1557
1558
|
end
|
|
1558
1559
|
|
|
1559
1560
|
PWN_AI_SLASH_COMMANDS = %w[
|
|
1560
|
-
/back /cron /debug /delegate /help /memory /sessions /skills
|
|
1561
|
+
/back /cron /debug /delegate /help /memory /model /sessions /skills
|
|
1561
1562
|
].freeze
|
|
1562
1563
|
|
|
1563
1564
|
PWN_AI_SLASH_SUBCOMMANDS = {
|
|
@@ -1566,6 +1567,7 @@ module PWN
|
|
|
1566
1567
|
'/delegate' => [],
|
|
1567
1568
|
'/help' => [],
|
|
1568
1569
|
'/memory' => %w[list recall remember forget clear],
|
|
1570
|
+
'/model' => %w[list],
|
|
1569
1571
|
'/sessions' => %w[list resume delete stats],
|
|
1570
1572
|
'/skills' => %w[list recall]
|
|
1571
1573
|
}.freeze
|
|
@@ -1618,6 +1620,20 @@ module PWN
|
|
|
1618
1620
|
|
|
1619
1621
|
cmd = tokens.first
|
|
1620
1622
|
sub_prefix = tokens.last.to_s
|
|
1623
|
+
if cmd == '/model'
|
|
1624
|
+
engines = pwn_ai_engines
|
|
1625
|
+
if tokens.length == 2
|
|
1626
|
+
pool = (%w[list] + engines)
|
|
1627
|
+
return pool.select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) }
|
|
1628
|
+
end
|
|
1629
|
+
return %w[llms].select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) } if tokens.length == 3 && tokens[1] == 'list'
|
|
1630
|
+
|
|
1631
|
+
if tokens.length >= 3
|
|
1632
|
+
current = pwn_ai_engine_model(engine: tokens[1]).to_s
|
|
1633
|
+
hits = [current].reject(&:empty?).select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) }
|
|
1634
|
+
return hits unless hits.empty?
|
|
1635
|
+
end
|
|
1636
|
+
end
|
|
1621
1637
|
subs = Array(PWN_AI_SLASH_SUBCOMMANDS[cmd])
|
|
1622
1638
|
hits = subs.select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) }
|
|
1623
1639
|
hits = [target] if hits.empty? && !target.empty?
|
|
@@ -1735,6 +1751,8 @@ module PWN
|
|
|
1735
1751
|
when '/delegate'
|
|
1736
1752
|
puts "[*] Delegating: #{args.join(' ')}"
|
|
1737
1753
|
puts ' Use agent_list / agent_debate from pwn-ai, or pwn-ai-delegate in the pwn REPL.'
|
|
1754
|
+
when '/model'
|
|
1755
|
+
pwn_ai_run_model(args: args)
|
|
1738
1756
|
end
|
|
1739
1757
|
true
|
|
1740
1758
|
rescue StandardError => e
|
|
@@ -1742,6 +1760,171 @@ module PWN
|
|
|
1742
1760
|
true
|
|
1743
1761
|
end
|
|
1744
1762
|
|
|
1763
|
+
public_class_method def self.pwn_ai_engines(opts = {})
|
|
1764
|
+
return [] unless opts.is_a?(Hash)
|
|
1765
|
+
|
|
1766
|
+
tmpl = {}
|
|
1767
|
+
tmpl = PWN::Config.env_template[:ai] if defined?(PWN::Config) && PWN::Config.respond_to?(:env_template)
|
|
1768
|
+
keys = tmpl.select { |_k, v| v.is_a?(Hash) && v.key?(:model) }.keys.map(&:to_s)
|
|
1769
|
+
if defined?(PWN::Env) && PWN::Env.is_a?(Hash) && PWN::Env[:ai].is_a?(Hash)
|
|
1770
|
+
PWN::Env[:ai].each do |k, v|
|
|
1771
|
+
next unless v.is_a?(Hash)
|
|
1772
|
+
next if %i[agent driver_opts].include?(k.to_sym)
|
|
1773
|
+
|
|
1774
|
+
keys << k.to_s if v.key?(:model) || v.key?(:key) || v.key?(:base_uri)
|
|
1775
|
+
end
|
|
1776
|
+
end
|
|
1777
|
+
keys.uniq.sort
|
|
1778
|
+
end
|
|
1779
|
+
|
|
1780
|
+
public_class_method def self.pwn_ai_provider_class(opts = {})
|
|
1781
|
+
engine = opts[:engine].to_s.downcase
|
|
1782
|
+
map = {
|
|
1783
|
+
'anthropic' => 'Anthropic',
|
|
1784
|
+
'gemini' => 'Gemini',
|
|
1785
|
+
'grok' => 'Grok',
|
|
1786
|
+
'ollama' => 'Ollama',
|
|
1787
|
+
'openai' => 'OpenAI',
|
|
1788
|
+
'openwebui' => 'OpenWebUI'
|
|
1789
|
+
}
|
|
1790
|
+
name = map[engine]
|
|
1791
|
+
return nil if name.nil? || !defined?(PWN::AI) || !PWN::AI.const_defined?(name)
|
|
1792
|
+
|
|
1793
|
+
PWN::AI.const_get(name)
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
public_class_method def self.pwn_ai_model_ids(opts = {})
|
|
1797
|
+
raw = opts[:models]
|
|
1798
|
+
rows = case raw
|
|
1799
|
+
when Array then raw
|
|
1800
|
+
when Hash then raw[:data] || raw[:models] || raw['data'] || raw['models'] || []
|
|
1801
|
+
else []
|
|
1802
|
+
end
|
|
1803
|
+
Array(rows).filter_map do |row|
|
|
1804
|
+
if row.is_a?(Hash)
|
|
1805
|
+
row[:id] || row['id'] || row[:name] || row['name'] || row[:model] || row['model']
|
|
1806
|
+
else
|
|
1807
|
+
row.to_s
|
|
1808
|
+
end
|
|
1809
|
+
end.map(&:to_s).reject(&:empty?).uniq
|
|
1810
|
+
end
|
|
1811
|
+
|
|
1812
|
+
public_class_method def self.pwn_ai_list_llms(opts = {})
|
|
1813
|
+
engine = opts[:engine].to_s
|
|
1814
|
+
engine = PWN::Env.dig(:ai, :active).to_s if engine.empty? && defined?(PWN::Env)
|
|
1815
|
+
raise 'no active engine — /model <engine> first' if engine.empty?
|
|
1816
|
+
|
|
1817
|
+
klass = pwn_ai_provider_class(engine: engine)
|
|
1818
|
+
raise "#{engine} has no PWN::AI provider with get_models" unless klass.respond_to?(:get_models)
|
|
1819
|
+
|
|
1820
|
+
ids = pwn_ai_model_ids(models: klass.get_models)
|
|
1821
|
+
puts "[*] #{engine} llms (#{ids.length})"
|
|
1822
|
+
ids.each { |id| puts id }
|
|
1823
|
+
ids
|
|
1824
|
+
end
|
|
1825
|
+
|
|
1826
|
+
public_class_method def self.pwn_ai_engine_model(opts = {})
|
|
1827
|
+
engine = opts[:engine].to_s.downcase.to_sym
|
|
1828
|
+
return '' if engine.empty?
|
|
1829
|
+
return '' unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
1830
|
+
|
|
1831
|
+
PWN::Env.dig(:ai, engine, :model).to_s
|
|
1832
|
+
end
|
|
1833
|
+
|
|
1834
|
+
public_class_method def self.pwn_ai_run_model(opts = {})
|
|
1835
|
+
args = Array(opts[:args]).map(&:to_s)
|
|
1836
|
+
engines = pwn_ai_engines
|
|
1837
|
+
current = defined?(PWN::Env) && PWN::Env.is_a?(Hash) ? PWN::Env.dig(:ai, :active).to_s : ''
|
|
1838
|
+
current_model = pwn_ai_engine_model(engine: current)
|
|
1839
|
+
sub = args[0].to_s
|
|
1840
|
+
if sub.empty? || %w[show status].include?(sub)
|
|
1841
|
+
msg = "active=#{current.empty? ? '(none)' : current} model=#{current_model.empty? ? '(unset)' : current_model}"
|
|
1842
|
+
puts "[*] #{msg}"
|
|
1843
|
+
return msg
|
|
1844
|
+
end
|
|
1845
|
+
if %w[list help].include?(sub)
|
|
1846
|
+
return pwn_ai_list_llms(engine: current) if args[1].to_s == 'llms'
|
|
1847
|
+
|
|
1848
|
+
puts 'pwn-ai /model — switch provider and model in this session'
|
|
1849
|
+
puts " current: #{current} #{current_model}"
|
|
1850
|
+
puts ' usage: /model [list] | /model list llms | /model <engine> [model] | /model <model>'
|
|
1851
|
+
engines.each do |eng|
|
|
1852
|
+
mark = eng == current ? '*' : ' '
|
|
1853
|
+
puts " #{mark} #{eng} #{pwn_ai_engine_model(engine: eng)}"
|
|
1854
|
+
end
|
|
1855
|
+
return engines
|
|
1856
|
+
end
|
|
1857
|
+
|
|
1858
|
+
engine = nil
|
|
1859
|
+
model = nil
|
|
1860
|
+
if engines.include?(sub)
|
|
1861
|
+
engine = sub
|
|
1862
|
+
model = args[1..].join(' ')
|
|
1863
|
+
model = nil if model.strip.empty?
|
|
1864
|
+
else
|
|
1865
|
+
engine = current
|
|
1866
|
+
model = args.join(' ')
|
|
1867
|
+
end
|
|
1868
|
+
raise "no active engine — /model <engine> first (#{engines.join(', ')})" if engine.to_s.empty?
|
|
1869
|
+
raise "unknown engine #{engine.inspect} — try: #{engines.join(', ')}" unless engines.include?(engine.to_s)
|
|
1870
|
+
|
|
1871
|
+
PWN::Env[:ai] ||= {}
|
|
1872
|
+
PWN::Env[:ai][engine.to_sym] ||= {}
|
|
1873
|
+
PWN::Env[:ai][:active] = engine.to_s
|
|
1874
|
+
PWN::Env[:ai][engine.to_sym][:model] = model unless model.to_s.strip.empty?
|
|
1875
|
+
persisted = persist_ai_selection(engine: engine, model: PWN::Env[:ai][engine.to_sym][:model])
|
|
1876
|
+
shown = PWN::Env[:ai][engine.to_sym][:model]
|
|
1877
|
+
msg = "active=#{engine} model=#{shown.to_s.empty? ? '(unset)' : shown}"
|
|
1878
|
+
msg = "#{msg} (session only)" unless persisted
|
|
1879
|
+
puts "[*] #{msg}"
|
|
1880
|
+
msg
|
|
1881
|
+
end
|
|
1882
|
+
|
|
1883
|
+
public_class_method def self.persist_ai_selection(opts = {})
|
|
1884
|
+
engine = opts[:engine].to_s
|
|
1885
|
+
model = opts[:model]
|
|
1886
|
+
return false if engine.empty?
|
|
1887
|
+
|
|
1888
|
+
env_path = nil
|
|
1889
|
+
dec_path = nil
|
|
1890
|
+
if defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
1891
|
+
env_path = PWN::Env.dig(:driver_opts, :pwn_env_path)
|
|
1892
|
+
dec_path = PWN::Env.dig(:driver_opts, :pwn_dec_path)
|
|
1893
|
+
end
|
|
1894
|
+
env_path = env_path.to_s.strip
|
|
1895
|
+
env_path = File.join(Dir.home, '.pwn', 'pwn.yaml') if env_path.empty?
|
|
1896
|
+
dec_path = dec_path.to_s.strip
|
|
1897
|
+
dec_path = "#{env_path}.decryptor" if dec_path.empty?
|
|
1898
|
+
return false unless File.exist?(env_path) && File.exist?(dec_path) && File.readable?(dec_path)
|
|
1899
|
+
|
|
1900
|
+
decryptor = YAML.load_file(dec_path, symbolize_names: true)
|
|
1901
|
+
key = decryptor.is_a?(Hash) ? decryptor[:key] : nil
|
|
1902
|
+
iv = decryptor.is_a?(Hash) ? decryptor[:iv] : nil
|
|
1903
|
+
return false if key.to_s.strip.empty? || iv.to_s.strip.empty?
|
|
1904
|
+
|
|
1905
|
+
PWN::Plugins::Vault.decrypt(file: env_path, key: key, iv: iv)
|
|
1906
|
+
begin
|
|
1907
|
+
cfg = YAML.load_file(env_path, symbolize_names: true)
|
|
1908
|
+
cfg = {} unless cfg.is_a?(Hash)
|
|
1909
|
+
cfg[:ai] = {} unless cfg[:ai].is_a?(Hash)
|
|
1910
|
+
cfg[:ai][:active] = engine
|
|
1911
|
+
unless model.to_s.strip.empty?
|
|
1912
|
+
slot = engine.to_sym
|
|
1913
|
+
cfg[:ai][slot] = {} unless cfg[:ai][slot].is_a?(Hash)
|
|
1914
|
+
cfg[:ai][slot][:model] = model
|
|
1915
|
+
end
|
|
1916
|
+
yaml_env = YAML.dump(cfg).gsub(/^(\s*):/, '\1')
|
|
1917
|
+
File.write(env_path, yaml_env)
|
|
1918
|
+
File.chmod(0o600, env_path)
|
|
1919
|
+
ensure
|
|
1920
|
+
PWN::Plugins::Vault.encrypt(file: env_path, key: key, iv: iv)
|
|
1921
|
+
end
|
|
1922
|
+
true
|
|
1923
|
+
rescue StandardError => e
|
|
1924
|
+
warn "[pwn-ai] /model persist skipped: #{e.class}: #{e.message}"
|
|
1925
|
+
false
|
|
1926
|
+
end
|
|
1927
|
+
|
|
1745
1928
|
public_class_method def self.pwn_ai_run_cron(opts = {})
|
|
1746
1929
|
args = Array(opts[:args])
|
|
1747
1930
|
sub = args[0] || 'list'
|
data/lib/pwn/version.rb
CHANGED
|
@@ -424,15 +424,23 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
424
424
|
expect(src).to match(/note_exception!/)
|
|
425
425
|
expect(src).to match(/rescue StandardError/)
|
|
426
426
|
expect(src).to include("debug_progress(msg: 'engine returned no message')")
|
|
427
|
+
expect(src).to match(/engine_transient|retryable\?/)
|
|
427
428
|
expect(src).not_to match(/dispatch #\{name\} ok_len=/)
|
|
428
429
|
end
|
|
429
430
|
|
|
430
|
-
it '
|
|
431
|
+
it 'keeps Loop.run going after a transient engine 504' do
|
|
432
|
+
src = File.read(described_class.method(:run).source_location.first)
|
|
433
|
+
expect(src).to match(/engine_transient\?|HttpRetry\.retryable\?/)
|
|
434
|
+
expect(src).to match(/engine hop failed|engine_blip/)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
it 'records a timeout increment mistake instead of treating success:true as ok' do
|
|
431
438
|
tmp = Dir.mktmpdir
|
|
432
439
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
440
|
+
PWN::AI::Agent::ToolGuard.reset_timeout_budget! if PWN::AI::Agent::ToolGuard.respond_to?(:reset_timeout_budget!)
|
|
433
441
|
raw = JSON.generate(
|
|
434
442
|
success: true,
|
|
435
|
-
result: { stdout: '', error: 'timeout after 20s', hint: 'x', scenario: '
|
|
443
|
+
result: { stdout: '', error: 'timeout after 20s', hint: 'x', scenario: 'deadline' }
|
|
436
444
|
)
|
|
437
445
|
tele = described_class.send(
|
|
438
446
|
:record_metrics,
|
|
@@ -443,7 +451,7 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
443
451
|
)
|
|
444
452
|
expect(tele[:ok]).to eq false
|
|
445
453
|
expect(tele[:mistake]).to be_a(Hash)
|
|
446
|
-
expect(tele[:mistake][:error].to_s).to match(/
|
|
454
|
+
expect(tele[:mistake][:error].to_s).to match(/timeout \+= 180|too short/i)
|
|
447
455
|
expect(tele[:mistake][:shape].to_s).to eq('timeout')
|
|
448
456
|
end
|
|
449
457
|
|
|
@@ -68,40 +68,101 @@ describe PWN::AI::Agent::ToolGuard do
|
|
|
68
68
|
expect(snap[:mem_avail_mb].to_i).to be >= 0
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
it '
|
|
71
|
+
it 'honors any explicit timeout up to 3 hours and derives a default when omitted' do
|
|
72
72
|
expect(described_class.deadline_s(timeout: 1, kind: :eval)).to eq(1)
|
|
73
|
-
expect(described_class.deadline_s(timeout:
|
|
74
|
-
expect(described_class.deadline_s(timeout:
|
|
73
|
+
expect(described_class.deadline_s(timeout: 300, kind: :eval)).to eq(300)
|
|
74
|
+
expect(described_class.deadline_s(timeout: 3_600, kind: :shell)).to eq(3_600)
|
|
75
|
+
expect(described_class.deadline_s(timeout: 99_999, kind: :eval)).to eq(10_800)
|
|
75
76
|
omitted = described_class.deadline_s(kind: :eval)
|
|
76
77
|
expect(omitted).to be_between(8, 90)
|
|
77
78
|
end
|
|
79
|
+
|
|
80
|
+
it 'does not sniff payloads for named tools' do
|
|
81
|
+
src = File.read(described_class.method(:deadline_s).source_location.first)
|
|
82
|
+
expect(src).not_to match(/\bnmap\b/i)
|
|
83
|
+
expect(src).not_to match(/\bsqlmap\b/i)
|
|
84
|
+
expect(src).not_to match(/\bhydra\b/i)
|
|
85
|
+
expect(described_class).not_to respond_to(:long_work?)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '.next_timeout' do
|
|
90
|
+
it 'adds 180 seconds until the 3-hour cap' do
|
|
91
|
+
expect(described_class.next_timeout(timeout: 60)).to eq(240)
|
|
92
|
+
expect(described_class.next_timeout(timeout: 180)).to eq(360)
|
|
93
|
+
expect(described_class.next_timeout(timeout: 10_800)).to eq(10_800)
|
|
94
|
+
end
|
|
78
95
|
end
|
|
79
96
|
|
|
80
97
|
describe '.timeout_lesson' do
|
|
81
|
-
|
|
98
|
+
before do
|
|
99
|
+
described_class.reset_timeout_budget!
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'increments timeout on the same payload before rewriting' do
|
|
82
103
|
tmp = Dir.mktmpdir
|
|
83
104
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
84
|
-
first = described_class.timeout_lesson(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
first = described_class.timeout_lesson(
|
|
106
|
+
tool: 'pwn_eval',
|
|
107
|
+
payload: 'sleep 3',
|
|
108
|
+
timeout: 180,
|
|
109
|
+
task: 't1'
|
|
110
|
+
)
|
|
111
|
+
expect(first[:scenario]).to eq(:deadline)
|
|
112
|
+
expect(first[:hint]).to match(/timeout \+= 180|next_timeout/i)
|
|
113
|
+
expect(first[:hint]).not_to match(/reconstruct/i)
|
|
114
|
+
expect(described_class.next_timeout(timeout: 180, spent: 180)).to eq(360)
|
|
89
115
|
end
|
|
90
116
|
|
|
91
|
-
it '
|
|
117
|
+
it 'rewrites the payload only after the 3-hour budget is exhausted' do
|
|
92
118
|
tmp = Dir.mktmpdir
|
|
93
119
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
94
|
-
|
|
120
|
+
described_class.note_timeout!(
|
|
121
|
+
tool: 'shell',
|
|
122
|
+
payload: 'sleep 3',
|
|
123
|
+
timeout: 10_800,
|
|
124
|
+
task: 't1'
|
|
125
|
+
)
|
|
126
|
+
lesson = described_class.timeout_lesson(
|
|
127
|
+
tool: 'shell',
|
|
128
|
+
payload: 'sleep 3',
|
|
129
|
+
timeout: 10_800,
|
|
130
|
+
task: 't1'
|
|
131
|
+
)
|
|
132
|
+
expect(lesson[:scenario]).to eq(:construction)
|
|
133
|
+
expect(lesson[:hint]).to match(/3-hour budget|Generate different/i)
|
|
134
|
+
expect(described_class.mutation_count(task: 't1')).to eq(1)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'stops after 10 mutations on the same task' do
|
|
138
|
+
10.times do |i|
|
|
139
|
+
described_class.note_timeout!(
|
|
140
|
+
tool: 'shell',
|
|
141
|
+
payload: "payload-#{i}",
|
|
142
|
+
timeout: 10_800,
|
|
143
|
+
task: 't1'
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
last = described_class.timeout_lesson(
|
|
147
|
+
tool: 'shell',
|
|
148
|
+
payload: 'payload-9',
|
|
149
|
+
timeout: 10_800,
|
|
150
|
+
task: 't1'
|
|
151
|
+
)
|
|
152
|
+
expect(described_class.mutation_count(task: 't1')).to eq(10)
|
|
153
|
+
expect(last[:scenario]).to eq(:exhausted)
|
|
154
|
+
expect(last[:hint]).to match(/10 mutations|mutation cap/i)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'includes next_timeout on a mid-budget timeout result' do
|
|
158
|
+
out = described_class.timeout_result(
|
|
95
159
|
tool: 'shell',
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
shape: :timeout
|
|
160
|
+
payload: 'sleep 3',
|
|
161
|
+
timeout: 180,
|
|
162
|
+
task: 't1'
|
|
100
163
|
)
|
|
101
|
-
|
|
102
|
-
expect(
|
|
103
|
-
expect(second[:hint]).to match(/timeout was too short|deadline too short|raise a conservative/i)
|
|
104
|
-
expect(second[:error]).to match(/too short/)
|
|
164
|
+
expect(out[:scenario]).to eq(:deadline)
|
|
165
|
+
expect(out[:next_timeout]).to eq(360)
|
|
105
166
|
end
|
|
106
167
|
end
|
|
107
168
|
end
|
|
@@ -49,8 +49,8 @@ describe 'PWN::AI::Agent::Tools ruby_eval' do
|
|
|
49
49
|
result = entry.handler.call(code: 'sleep 3', timeout: 1)
|
|
50
50
|
expect(Time.now - t0).to be < 2.5
|
|
51
51
|
expect(result[:error].to_s).to match(/timeout after 1s/)
|
|
52
|
-
expect(result[:hint].to_s).to match(/
|
|
53
|
-
expect(result[:scenario].to_s).to eq('
|
|
52
|
+
expect(result[:hint].to_s).to match(/timeout \+= 180|next_timeout|same .*payload/i)
|
|
53
|
+
expect(result[:scenario].to_s).to eq('deadline')
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
it 'Dispatch.call JSON-wraps syntax errors instead of raising' do
|
|
@@ -60,12 +60,12 @@ describe 'PWN::AI::Agent::Tools shell' do
|
|
|
60
60
|
expect(r[:exit]).to eq(0)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
it 'on timeout
|
|
63
|
+
it 'on timeout keeps the same command and increments timeout' do
|
|
64
64
|
tmp = Dir.mktmpdir
|
|
65
65
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
66
66
|
r = handler.call(command: 'sleep 3', timeout: 1)
|
|
67
67
|
expect(r[:error].to_s).to match(/timeout after 1s/)
|
|
68
|
-
expect(r[:hint].to_s).to match(/
|
|
69
|
-
expect(r[:scenario].to_s).to eq('
|
|
68
|
+
expect(r[:hint].to_s).to match(/timeout \+= 180|next_timeout|same .*payload/i)
|
|
69
|
+
expect(r[:scenario].to_s).to eq('deadline')
|
|
70
70
|
end
|
|
71
71
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
require 'stringio'
|
|
5
|
+
require 'rest-client'
|
|
5
6
|
|
|
6
7
|
describe PWN::AI::HttpRetry do
|
|
7
8
|
it 'defaults timeout to 180 and max_attempts to 5' do
|
|
@@ -14,6 +15,12 @@ describe PWN::AI::HttpRetry do
|
|
|
14
15
|
expect(described_class.max_attempts(max_attempts: 3)).to eq(3)
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
it 'treats gateway 504 and stream absolute timeouts as retryable' do
|
|
19
|
+
expect(described_class.retryable?(error: 'ERROR: Ollama HTTP 504 for api/chat: Gateway Time-out')).to eq(true)
|
|
20
|
+
expect(described_class.retryable?(error: 'ERROR: Ollama stream absolute timeout after 20s for api/chat')).to eq(true)
|
|
21
|
+
expect(described_class.retryable?(error: '400 Bad Request')).to eq(false)
|
|
22
|
+
end
|
|
23
|
+
|
|
17
24
|
it 'tees emit lines into the open debug request log' do
|
|
18
25
|
path = PWN::Plugins::Log.start_debug(
|
|
19
26
|
tee: StringIO.new,
|
|
@@ -50,6 +57,7 @@ describe PWN::AI::HttpRetry do
|
|
|
50
57
|
expect(src).not_to match(/timeout \|\|= 900/)
|
|
51
58
|
expect(src).to match(/Exceptions::Timeout/)
|
|
52
59
|
expect(src).to match(/HttpRetry\.report_event/)
|
|
60
|
+
expect(src).to match(/retryable\?|HTTP 50/) if name == 'PWN::AI::Ollama'
|
|
53
61
|
end
|
|
54
62
|
end
|
|
55
63
|
end
|
|
@@ -12,4 +12,11 @@ describe PWN::Plugins::NmapIt do
|
|
|
12
12
|
help_response = PWN::Plugins::NmapIt
|
|
13
13
|
expect(help_response).to respond_to :help
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
it 'port_scan accepts opts = {} and aliases xml to output_xml' do
|
|
17
|
+
expect(described_class.method(:port_scan).parameters).to include(%i[opt opts])
|
|
18
|
+
src = File.read(described_class.method(:port_scan).source_location.first)
|
|
19
|
+
expect(src).to include('opts[')
|
|
20
|
+
expect(src).to include('output_xml')
|
|
21
|
+
end
|
|
15
22
|
end
|
|
@@ -174,4 +174,14 @@ describe 'pwn-ai debug final answer placement' do
|
|
|
174
174
|
expect(hook).to include('→ pwn-ai →')
|
|
175
175
|
expect(hook).to include('→ result')
|
|
176
176
|
end
|
|
177
|
+
|
|
178
|
+
it 'does not fall through to regex-ReAct after a native Loop.run error' do
|
|
179
|
+
native_start = hook.index('final = PWN::AI::Agent::Loop.run')
|
|
180
|
+
rescue_at = hook.index('rescue StandardError', native_start)
|
|
181
|
+
expect(rescue_at).not_to be_nil
|
|
182
|
+
chunk = hook[rescue_at, 900]
|
|
183
|
+
expect(chunk).to match(/request\.replace\('nil'\)/)
|
|
184
|
+
expect(chunk).to match(/\bnext\b/)
|
|
185
|
+
expect(chunk).not_to include('legacy regex-ReAct')
|
|
186
|
+
end
|
|
177
187
|
end
|
|
@@ -67,11 +67,15 @@ describe PWN::Plugins::REPL do
|
|
|
67
67
|
hits = described_class.pwn_ai_complete(target: '/sk', line: '/sk')
|
|
68
68
|
expect(hits).to include('/skills')
|
|
69
69
|
hits = described_class.pwn_ai_complete(target: '/', line: '/')
|
|
70
|
-
%w[/cron /skills /sessions /memory /debug /back /help].each do |cmd|
|
|
70
|
+
%w[/cron /skills /sessions /memory /debug /back /help /model].each do |cmd|
|
|
71
71
|
expect(hits).to include(cmd)
|
|
72
72
|
end
|
|
73
73
|
hits = described_class.pwn_ai_complete(target: 'li', line: '/cron li')
|
|
74
74
|
expect(hits).to include('list')
|
|
75
|
+
hits = described_class.pwn_ai_complete(target: 'li', line: '/model li')
|
|
76
|
+
expect(hits).to include('list')
|
|
77
|
+
hits = described_class.pwn_ai_complete(target: 'll', line: '/model list ll')
|
|
78
|
+
expect(hits).to include('llms')
|
|
75
79
|
end
|
|
76
80
|
|
|
77
81
|
it 'completes host-native paths when slash is not the first character' do
|
|
@@ -101,5 +105,37 @@ describe PWN::Plugins::REPL do
|
|
|
101
105
|
expect(hook).to match(/pwn_ai_dispatch_slash!/)
|
|
102
106
|
expect(described_class).to respond_to(:pwn_ai_dispatch_slash!)
|
|
103
107
|
end
|
|
108
|
+
|
|
109
|
+
it 'switches the live engine and model via /model without Loop.run' do
|
|
110
|
+
expect(described_class).to respond_to(:pwn_ai_run_model)
|
|
111
|
+
PWN::Env[:ai] ||= {}
|
|
112
|
+
PWN::Env[:ai][:grok] ||= {}
|
|
113
|
+
prev_active = PWN::Env[:ai][:active]
|
|
114
|
+
prev_model = PWN::Env[:ai][:grok][:model]
|
|
115
|
+
allow(described_class).to receive(:persist_ai_selection).and_return(false)
|
|
116
|
+
out = described_class.pwn_ai_run_model(args: %w[grok pwn-ai-test-model])
|
|
117
|
+
expect(PWN::Env[:ai][:active].to_s).to eq('grok')
|
|
118
|
+
expect(PWN::Env[:ai][:grok][:model]).to eq('pwn-ai-test-model')
|
|
119
|
+
expect(out.to_s).to match(/grok/i)
|
|
120
|
+
ensure
|
|
121
|
+
if PWN::Env.is_a?(Hash) && PWN::Env[:ai].is_a?(Hash)
|
|
122
|
+
PWN::Env[:ai][:active] = prev_active if defined?(prev_active)
|
|
123
|
+
PWN::Env[:ai][:grok][:model] = prev_model if defined?(prev_model) && PWN::Env[:ai][:grok].is_a?(Hash)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'lists llm ids from the active provider via /model list llms' do
|
|
128
|
+
expect(described_class).to respond_to(:pwn_ai_list_llms)
|
|
129
|
+
PWN::Env[:ai] ||= {}
|
|
130
|
+
prev_active = PWN::Env[:ai][:active]
|
|
131
|
+
PWN::Env[:ai][:active] = 'grok'
|
|
132
|
+
allow(PWN::AI::Grok).to receive(:get_models).and_return(
|
|
133
|
+
[{ id: 'grok-test-a' }, { id: 'grok-test-b' }]
|
|
134
|
+
)
|
|
135
|
+
ids = described_class.pwn_ai_run_model(args: %w[list llms])
|
|
136
|
+
expect(ids).to eq(%w[grok-test-a grok-test-b])
|
|
137
|
+
ensure
|
|
138
|
+
PWN::Env[:ai][:active] = prev_active if PWN::Env.is_a?(Hash) && PWN::Env[:ai].is_a?(Hash)
|
|
139
|
+
end
|
|
104
140
|
end
|
|
105
141
|
end
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -339,6 +339,7 @@
|
|
|
339
339
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.degrade_text_only Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.degrade_text_only`: "}]}
|
|
340
340
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.dispatch_fail_n Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.dispatch_fail_n`: "}]}
|
|
341
341
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.emit_task_summary Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.emit_task_summary`: "}]}
|
|
342
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.engine_transient? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.engine_transient?`: "}]}
|
|
342
343
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.escalate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.escalate`: "}]}
|
|
343
344
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_enough_to_finalize? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_enough_to_finalize?`: "}]}
|
|
344
345
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.expose_current_session Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.expose_current_session`: "}]}
|
|
@@ -383,6 +384,7 @@
|
|
|
383
384
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.task_summary_flush! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.task_summary_flush!`: "}]}
|
|
384
385
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.task_summary_plan! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.task_summary_plan!`: "}]}
|
|
385
386
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.task_summary_record! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.task_summary_record!`: "}]}
|
|
387
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.timeout_task Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.timeout_task`: "}]}
|
|
386
388
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.tool_effects Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.tool_effects`: "}]}
|
|
387
389
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.world_knowledge? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.world_knowledge?`: "}]}
|
|
388
390
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_verified? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_verified?`: "}]}
|
|
@@ -706,18 +708,30 @@
|
|
|
706
708
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TaskSummarizer.why_bit Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TaskSummarizer.why_bit`: "}]}
|
|
707
709
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.authors`: "}]}
|
|
708
710
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.bashism? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.bashism?`: "}]}
|
|
711
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.budget_exhausted? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.budget_exhausted?`: "}]}
|
|
709
712
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.coerce_args Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.coerce_args`: "}]}
|
|
710
713
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.deadline_s Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.deadline_s`: "}]}
|
|
711
714
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.help`: "}]}
|
|
712
715
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.host_load Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.host_load`: "}]}
|
|
713
716
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.invalid_payload Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.invalid_payload`: "}]}
|
|
717
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.mutation_count Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.mutation_count`: "}]}
|
|
718
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.next_timeout Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.next_timeout`: "}]}
|
|
719
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.note_timeout! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.note_timeout!`: "}]}
|
|
720
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.payload_key Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.payload_key`: "}]}
|
|
721
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.payload_spent Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.payload_spent`: "}]}
|
|
714
722
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.placeholder? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.placeholder?`: "}]}
|
|
715
723
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.present? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.present?`: "}]}
|
|
724
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.reset_timeout_budget Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.reset_timeout_budget`: "}]}
|
|
725
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.reset_timeout_budget! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.reset_timeout_budget!`: "}]}
|
|
716
726
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.shell_bash? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.shell_bash?`: "}]}
|
|
717
727
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.shell_name Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.shell_name`: "}]}
|
|
728
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.task_key Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.task_key`: "}]}
|
|
718
729
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_lesson Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_lesson`: "}]}
|
|
730
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_mutated Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_mutated`: "}]}
|
|
731
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_mutations Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_mutations`: "}]}
|
|
719
732
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_prior_count Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_prior_count`: "}]}
|
|
720
733
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_result Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_result`: "}]}
|
|
734
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_spent Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_spent`: "}]}
|
|
721
735
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TransparentBrowser.analyze Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TransparentBrowser.analyze`: Supported Method Parameters\n\nai_analysis = PWN::AI::Agent::TransparentBrowser.analyze(\n\nrequest: 'required - current step in the JavaScript debugging session to analyze',\nsource_to_review: 'required - the block of JavaScript code in which the current step resides'\n\n)\n"}]}
|
|
722
736
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TransparentBrowser.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TransparentBrowser.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
723
737
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::TransparentBrowser.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TransparentBrowser.help`: "}]}
|
|
@@ -780,6 +794,7 @@
|
|
|
780
794
|
{"messages":[{"role":"user","content":"PWN::AI::HttpRetry.max_attempts Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.max_attempts`: "}]}
|
|
781
795
|
{"messages":[{"role":"user","content":"PWN::AI::HttpRetry.report_event Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.report_event`: "}]}
|
|
782
796
|
{"messages":[{"role":"user","content":"PWN::AI::HttpRetry.retry_after_s Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.retry_after_s`: "}]}
|
|
797
|
+
{"messages":[{"role":"user","content":"PWN::AI::HttpRetry.retryable? Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.retryable?`: "}]}
|
|
783
798
|
{"messages":[{"role":"user","content":"PWN::AI::HttpRetry.timeout_s Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.timeout_s`: "}]}
|
|
784
799
|
{"messages":[{"role":"user","content":"PWN::AI::Ollama.assemble_native_chat_stream Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.assemble_native_chat_stream`: "}]}
|
|
785
800
|
{"messages":[{"role":"user","content":"PWN::AI::Ollama.assemble_ollama_stream Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.assemble_ollama_stream`: "}]}
|
|
@@ -2140,11 +2155,13 @@
|
|
|
2140
2155
|
{"messages":[{"role":"user","content":"PWN::Plugins::NexposeVulnScan.logout Usage"},{"role":"assistant","content":"`PWN::Plugins::NexposeVulnScan.logout`: Supported Method Parameters\n\nPWN::Plugins::NexposeVulnScan.logout(\n\nnsc_obj: 'required nsc_obj returned from login method'\n\n)\n"}]}
|
|
2141
2156
|
{"messages":[{"role":"user","content":"PWN::Plugins::NexposeVulnScan.scan_site_by_name Usage"},{"role":"assistant","content":"`PWN::Plugins::NexposeVulnScan.scan_site_by_name`: Supported Method Parameters\n\nPWN::Plugins::NexposeVulnScan.scan_site_by_name(\n\nnsc_obj: 'required nsc_obj returned from login method',\nsite_name: 'required Nexpose site name to scan (case-sensitive),\npoll_interval: 'optional poll interval to check the completion status of the scan (defaults to 3 minutes)\n\n)\n"}]}
|
|
2142
2157
|
{"messages":[{"role":"user","content":"PWN::Plugins::NexposeVulnScan.update_site_assets Usage"},{"role":"assistant","content":"`PWN::Plugins::NexposeVulnScan.update_site_assets`: Supported Method Parameters\n\nPWN::Plugins::NexposeVulnScan.update_site_assets(\n\nnsc_obj: 'required nsc_obj returned from login method',\nsite_name: 'required Nexpose site name to update (case-sensitive),\nassets: 'required array of hashes containing called :ip => values being IP address (All IPs not included in the :assets parameter will be removed from Nexpose)'\n\n)\n"}]}
|
|
2158
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.apply_port_scan_opts Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.apply_port_scan_opts`: "}]}
|
|
2143
2159
|
{"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
2144
2160
|
{"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.diff_xml_results Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.diff_xml_results`: Supported Method Parameters\n\nPWN::Plugins::NmapIt.diff_xml_results(\n\nxml_a: 'required - path to nmap xml results',\nxml_b: 'required - path to nmap xml results',\ndiff: 'required - path to nmap xml results diff'\n\n)\n"}]}
|
|
2145
2161
|
{"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.help Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.help`: "}]}
|
|
2162
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.nmap_compat Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.nmap_compat`: "}]}
|
|
2146
2163
|
{"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.parse_xml_results Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.parse_xml_results`: Supported Method Parameters\n\nPWN::Plugins::NmapIt.parse_xml_results(:xml_file => ‘required - path to nmap xml results’) do |xml|\n\nputs xml.public_methods\nxml.each_host do |host|\n puts \"[#{host.ip}]\"\n\n host.scripts.each do |name,output|\n output.each_line { |line| puts \" #{line}\" }\n end\n\n host.each_port do |port|\n puts \" [#{port.number}/#{port.protocol}]\"\n\n port.scripts.each do |name,output|\n puts \" [#{name}]\"\n output.each_line { |line| puts \" #{line}\" }\n end\n end\nend\n\n\nend\n"}]}
|
|
2147
|
-
{"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.port_scan Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.port_scan`: Supported Method Parameters\n\nPWN::Plugins::NmapIt.port_scan do |nmap|\n\nputs nmap.public_methods\nnmap.connect_scan = true\nnmap.service_scan = true\nnmap.verbose = true\nnmap.ports = [1..1024,1337]\nnmap.targets = '127.0.0.1'\nnmap.xml = '/tmp/nmap_port_scan_res.xml'\n\n\nend\n"}]}
|
|
2164
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.port_scan Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.port_scan`: Supported Method Parameters\n\nPWN::Plugins::NmapIt.port_scan do |nmap|\n\nputs nmap.public_methods\nnmap.connect_scan = true\nnmap.service_scan = true\nnmap.verbose = true\nnmap.ports = [1..1024, 1337]\nnmap.targets = '127.0.0.1'\nnmap.xml = '/tmp/nmap_port_scan_res.xml' # alias of output_xml\n\n\nend\n\nPWN::Plugins::NmapIt.port_scan(\n\ntargets: '127.0.0.1',\nports: '1-65535',\nconnect_scan: true,\nservice_scan: true,\nscript: 'vuln,safe',\nxml: '/tmp/nmap_port_scan_res.xml'\n\n)\n"}]}
|
|
2148
2165
|
{"messages":[{"role":"user","content":"PWN::Plugins::OAuth2.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::OAuth2.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
2149
2166
|
{"messages":[{"role":"user","content":"PWN::Plugins::OAuth2.decode Usage"},{"role":"assistant","content":"`PWN::Plugins::OAuth2.decode`: Supported Method Parameters\n\nPWN::Plugins::OAuth2.decode(\n\noauth2_token: 'required oauth2 token'\n\n)\n"}]}
|
|
2150
2167
|
{"messages":[{"role":"user","content":"PWN::Plugins::OAuth2.get_value_by_key Usage"},{"role":"assistant","content":"`PWN::Plugins::OAuth2.get_value_by_key`: Supported Method Parameters\n\nPWN::Plugins::OAuth2.get_value_by_key(\n\noauth2_token: 'required oauth2 token',\nkey: 'required oauth2 token key name located within the Base64 encoded token as symbol, e.g. :company_id'\n\n)\n"}]}
|
|
@@ -2228,14 +2245,21 @@
|
|
|
2228
2245
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.enable_autocomplete Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.enable_autocomplete`: Supported Method Parameters\n\nPWN::Plugins::REPL.enable_autocomplete(\n\nenabled: 'optional - Boolean (default true). false reverts to single-line cycling.'\n\n)\n\nIRB-style suggest-as-you-type for the pwn REPL.\n\nReplaces Pry’s default input (rb-readline — single-candidate TAB cycling) with Reline and turns on Reline.autocompletion, which renders a live dropdown of candidates below the cursor as you type (the same widget IRB uses). Pry already wires Reline.completion_proc → @pry.complete (Pry::InputCompleter) when input == Reline, so the menu is fed by the full Ruby/PWN object graph: constants (PWN::Plugins::Nm<TAB>), instance methods, local/global variables, and Pry slash-commands.\n\nNavigate with ↑/↓ or TAB, accept with → or ENTER, dismiss with ESC.\n\nScope: this drives the MAIN pwn REPL (Ruby). pwn-ai swaps PWNMultiLineInput onto Reline.readmultiline and installs install_pwn_ai_completer! so TAB is command / path / Ruby menus. pwn-asm keeps opcode input without those menus.\n"}]}
|
|
2229
2246
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.help Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.help`: "}]}
|
|
2230
2247
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.install_pwn_ai_completer! Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.install_pwn_ai_completer!`: "}]}
|
|
2248
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.persist_ai_selection Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.persist_ai_selection`: "}]}
|
|
2231
2249
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_complete Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_complete`: Supported Method Parameters\n\nhits = PWN::Plugins::REPL.pwn_ai_complete(\n\ntarget: 'required - token Reline is completing',\nline: 'optional - full line buffer',\npry: 'optional - Pry instance for Ruby completion'\n\n)\n"}]}
|
|
2232
2250
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_complete_command Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_complete_command`: "}]}
|
|
2233
2251
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_complete_kind Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_complete_kind`: Supported Method Parameters\n\nkind = PWN::Plugins::REPL.pwn_ai_complete_kind(line: ‘optional - current input buffer’)\n\nfirst char is ‘/’ → :command (pwn-ai slash menu)\n\n‘/’ anywhere else → :path (host-native file nav)\n\nelse → :ruby (same Pry::InputCompleter menu as the pwn REPL)\n\n"}]}
|
|
2234
2252
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_complete_path Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_complete_path`: "}]}
|
|
2235
2253
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_complete_ruby Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_complete_ruby`: "}]}
|
|
2236
2254
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_dispatch_slash! Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_dispatch_slash!`: "}]}
|
|
2255
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_engine_model Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_engine_model`: "}]}
|
|
2256
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_engines Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_engines`: "}]}
|
|
2257
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_list_llms Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_list_llms`: "}]}
|
|
2258
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_model_ids Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_model_ids`: "}]}
|
|
2259
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_provider_class Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_provider_class`: "}]}
|
|
2237
2260
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_run_cron Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_run_cron`: "}]}
|
|
2238
2261
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_run_memory Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_run_memory`: "}]}
|
|
2262
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_run_model Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_run_model`: "}]}
|
|
2239
2263
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_run_sessions Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_run_sessions`: "}]}
|
|
2240
2264
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_run_skills Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_run_skills`: "}]}
|
|
2241
2265
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL.ready_tty! Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.ready_tty!`: "}]}
|