pwn 0.5.696 → 0.5.697

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74bfcd98ff2b783dab4bfc6d97a3f8124e00c395f686c05303002e312d5b9596
4
- data.tar.gz: 54d5d1abe1a695c1fb9dd7757becf2648857ff46dd96d9462ee5f73219a13146
3
+ metadata.gz: 4218feff0995079766c3b31b6beec85c9b45b1f971ac0bc78a9794c67ea8016e
4
+ data.tar.gz: 5a47207b366764a60b8c560bdcf77c5b12288d7fdbeb3e2d74c655a8e92e12e3
5
5
  SHA512:
6
- metadata.gz: b60265a293cf56ae14e0ff8d3f822faa36ea0b530f6d01c2a9bbd5b2c3256ab3525df45f964a13939c77672c76e4c0b67647baeca3c1faf2f987ff9221a04999
7
- data.tar.gz: b600f46b403f45c2418640a9047653759200d4aca46d2d0571992e6ae1d8501f40c1b28fb9515c2d38b9f313932b4441443b57c61c250b0560b7d5466df7bf43
6
+ metadata.gz: 326c061669305b7069c4a66e68d958b6935ef3a9c3f90e02ee1242ad6bedfed9e5e31170fd4ae8b7b9570ab7c63fb1e7198c0cc97a5a8e3eeb96400bc329c611
7
+ data.tar.gz: ed8cdb221e58fdb00ae799d902121e0f2b6f629871ceb458a54ef35ee5e2ad326070c431d9c17346db442e0d62374f90f85cba074a00967d3193d41753a091a7
@@ -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`
@@ -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,21 @@ 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
+
838
854
  # E1 — did the environment change under this tool? If Metrics CUSUM
839
855
  # tripped for it in the last hour AND Extrospection.drift shows a
840
856
  # toolchain/net/repo change, blame the WORLD not the AGENT.
@@ -2271,6 +2287,7 @@ module PWN
2271
2287
  start_debug_session(opts)
2272
2288
  loud_debug_tui!(debug: opts[:debug])
2273
2289
  debug_progress(msg: "Loop.run start request=#{request[0, 240]}", debug: opts[:debug])
2290
+ ToolGuard.reset_timeout_budget! if defined?(ToolGuard) && ToolGuard.respond_to?(:reset_timeout_budget!)
2274
2291
  nested = defined?(TurnFinalizer) && TurnFinalizer.user_path?
2275
2292
  TurnFinalizer.enter_user_path! if defined?(TurnFinalizer)
2276
2293
  engine = active_engine
@@ -100,9 +100,10 @@ module PWN
100
100
 
101
101
  HOST LOAD
102
102
  #{host_load_block}
103
- On pwn_eval/shell timeout: reconstruct the ruby/command for the
104
- same goal first. Do not first raise timeout. Only if construction
105
- is sound, raise a conservative HOST LOAD timeout (clamped).
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
@@ -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 model estimate is honored then clamped. Omit → host-derived
120
- # default from loadavg / ncpu / MemAvailable (loaded hosts get a bit
121
- # more time, still capped).
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, max)
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
- # Two-scenario timeout policy (loop-law, not a skill):
145
- # 1. First timeout reconstruct the ruby/command for the same goal.
146
- # Do not first raise timeout.
147
- # 2. A later timeout of the same tool → deadline was too short; raise
148
- # a conservative HOST LOAD timeout (still clamped).
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
- prior = timeout_prior_count(tool: tool)
155
- if prior.positive?
156
- {
157
- scenario: :deadline,
158
- error: "#{tool} timeout: deadline too short; raise conservative HOST LOAD timeout",
159
- hint: "Scenario 2: the #{tool} payload already timed out once. " \
160
- "This timeout (#{timeout}s) was too short. Raise a conservative " \
161
- 'timeout from HOST LOAD (clamped). Record this so it does not recur. ' \
162
- 'Do not retry the identical payload at the same deadline.'
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: :construction,
167
- error: "#{tool} timeout: reconstruct payload to same goal before raising timeout",
168
- hint: "Scenario 1 (try first): the #{tool} ruby/command was constructed " \
169
- 'improperly. Generate it differently to achieve the same goal. ' \
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: :construction, hint: '', shell: shell_name } unless opts.is_a?(Hash)
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: opts[: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 #{opts[:timeout].to_i}s",
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 (clamped). ' \
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. Clamped 1..90.'
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
- lesson = PWN::AI::Agent::ToolGuard.timeout_lesson(
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 (clamped).',
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. Clamped 1..180.'
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. ' \
@@ -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
- yield(nmap)
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
@@ -1557,7 +1557,7 @@ module PWN
1557
1557
  end
1558
1558
 
1559
1559
  PWN_AI_SLASH_COMMANDS = %w[
1560
- /back /cron /debug /delegate /help /memory /sessions /skills
1560
+ /back /cron /debug /delegate /help /memory /model /sessions /skills
1561
1561
  ].freeze
1562
1562
 
1563
1563
  PWN_AI_SLASH_SUBCOMMANDS = {
@@ -1566,6 +1566,7 @@ module PWN
1566
1566
  '/delegate' => [],
1567
1567
  '/help' => [],
1568
1568
  '/memory' => %w[list recall remember forget clear],
1569
+ '/model' => %w[list],
1569
1570
  '/sessions' => %w[list resume delete stats],
1570
1571
  '/skills' => %w[list recall]
1571
1572
  }.freeze
@@ -1618,6 +1619,20 @@ module PWN
1618
1619
 
1619
1620
  cmd = tokens.first
1620
1621
  sub_prefix = tokens.last.to_s
1622
+ if cmd == '/model'
1623
+ engines = pwn_ai_engines
1624
+ if tokens.length == 2
1625
+ pool = (%w[list] + engines)
1626
+ return pool.select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) }
1627
+ end
1628
+ return %w[llms].select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) } if tokens.length == 3 && tokens[1] == 'list'
1629
+
1630
+ if tokens.length >= 3
1631
+ current = pwn_ai_engine_model(engine: tokens[1]).to_s
1632
+ hits = [current].reject(&:empty?).select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) }
1633
+ return hits unless hits.empty?
1634
+ end
1635
+ end
1621
1636
  subs = Array(PWN_AI_SLASH_SUBCOMMANDS[cmd])
1622
1637
  hits = subs.select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) }
1623
1638
  hits = [target] if hits.empty? && !target.empty?
@@ -1735,6 +1750,8 @@ module PWN
1735
1750
  when '/delegate'
1736
1751
  puts "[*] Delegating: #{args.join(' ')}"
1737
1752
  puts ' Use agent_list / agent_debate from pwn-ai, or pwn-ai-delegate in the pwn REPL.'
1753
+ when '/model'
1754
+ pwn_ai_run_model(args: args)
1738
1755
  end
1739
1756
  true
1740
1757
  rescue StandardError => e
@@ -1742,6 +1759,171 @@ module PWN
1742
1759
  true
1743
1760
  end
1744
1761
 
1762
+ public_class_method def self.pwn_ai_engines(opts = {})
1763
+ return [] unless opts.is_a?(Hash)
1764
+
1765
+ tmpl = {}
1766
+ tmpl = PWN::Config.env_template[:ai] if defined?(PWN::Config) && PWN::Config.respond_to?(:env_template)
1767
+ keys = tmpl.select { |_k, v| v.is_a?(Hash) && v.key?(:model) }.keys.map(&:to_s)
1768
+ if defined?(PWN::Env) && PWN::Env.is_a?(Hash) && PWN::Env[:ai].is_a?(Hash)
1769
+ PWN::Env[:ai].each do |k, v|
1770
+ next unless v.is_a?(Hash)
1771
+ next if %i[agent driver_opts].include?(k.to_sym)
1772
+
1773
+ keys << k.to_s if v.key?(:model) || v.key?(:key) || v.key?(:base_uri)
1774
+ end
1775
+ end
1776
+ keys.uniq.sort
1777
+ end
1778
+
1779
+ public_class_method def self.pwn_ai_provider_class(opts = {})
1780
+ engine = opts[:engine].to_s.downcase
1781
+ map = {
1782
+ 'anthropic' => 'Anthropic',
1783
+ 'gemini' => 'Gemini',
1784
+ 'grok' => 'Grok',
1785
+ 'ollama' => 'Ollama',
1786
+ 'openai' => 'OpenAI',
1787
+ 'openwebui' => 'OpenWebUI'
1788
+ }
1789
+ name = map[engine]
1790
+ return nil if name.nil? || !defined?(PWN::AI) || !PWN::AI.const_defined?(name)
1791
+
1792
+ PWN::AI.const_get(name)
1793
+ end
1794
+
1795
+ public_class_method def self.pwn_ai_model_ids(opts = {})
1796
+ raw = opts[:models]
1797
+ rows = case raw
1798
+ when Array then raw
1799
+ when Hash then raw[:data] || raw[:models] || raw['data'] || raw['models'] || []
1800
+ else []
1801
+ end
1802
+ Array(rows).filter_map do |row|
1803
+ if row.is_a?(Hash)
1804
+ row[:id] || row['id'] || row[:name] || row['name'] || row[:model] || row['model']
1805
+ else
1806
+ row.to_s
1807
+ end
1808
+ end.map(&:to_s).reject(&:empty?).uniq
1809
+ end
1810
+
1811
+ public_class_method def self.pwn_ai_list_llms(opts = {})
1812
+ engine = opts[:engine].to_s
1813
+ engine = PWN::Env.dig(:ai, :active).to_s if engine.empty? && defined?(PWN::Env)
1814
+ raise 'no active engine — /model <engine> first' if engine.empty?
1815
+
1816
+ klass = pwn_ai_provider_class(engine: engine)
1817
+ raise "#{engine} has no PWN::AI provider with get_models" unless klass.respond_to?(:get_models)
1818
+
1819
+ ids = pwn_ai_model_ids(models: klass.get_models)
1820
+ puts "[*] #{engine} llms (#{ids.length})"
1821
+ ids.each { |id| puts id }
1822
+ ids
1823
+ end
1824
+
1825
+ public_class_method def self.pwn_ai_engine_model(opts = {})
1826
+ engine = opts[:engine].to_s.downcase.to_sym
1827
+ return '' if engine.empty?
1828
+ return '' unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
1829
+
1830
+ PWN::Env.dig(:ai, engine, :model).to_s
1831
+ end
1832
+
1833
+ public_class_method def self.pwn_ai_run_model(opts = {})
1834
+ args = Array(opts[:args]).map(&:to_s)
1835
+ engines = pwn_ai_engines
1836
+ current = defined?(PWN::Env) && PWN::Env.is_a?(Hash) ? PWN::Env.dig(:ai, :active).to_s : ''
1837
+ current_model = pwn_ai_engine_model(engine: current)
1838
+ sub = args[0].to_s
1839
+ if sub.empty? || %w[show status].include?(sub)
1840
+ msg = "active=#{current.empty? ? '(none)' : current} model=#{current_model.empty? ? '(unset)' : current_model}"
1841
+ puts "[*] #{msg}"
1842
+ return msg
1843
+ end
1844
+ if %w[list help].include?(sub)
1845
+ return pwn_ai_list_llms(engine: current) if args[1].to_s == 'llms'
1846
+
1847
+ puts 'pwn-ai /model — switch provider and model in this session'
1848
+ puts " current: #{current} #{current_model}"
1849
+ puts ' usage: /model [list] | /model list llms | /model <engine> [model] | /model <model>'
1850
+ engines.each do |eng|
1851
+ mark = eng == current ? '*' : ' '
1852
+ puts " #{mark} #{eng} #{pwn_ai_engine_model(engine: eng)}"
1853
+ end
1854
+ return engines
1855
+ end
1856
+
1857
+ engine = nil
1858
+ model = nil
1859
+ if engines.include?(sub)
1860
+ engine = sub
1861
+ model = args[1..].join(' ')
1862
+ model = nil if model.strip.empty?
1863
+ else
1864
+ engine = current
1865
+ model = args.join(' ')
1866
+ end
1867
+ raise "no active engine — /model <engine> first (#{engines.join(', ')})" if engine.to_s.empty?
1868
+ raise "unknown engine #{engine.inspect} — try: #{engines.join(', ')}" unless engines.include?(engine.to_s)
1869
+
1870
+ PWN::Env[:ai] ||= {}
1871
+ PWN::Env[:ai][engine.to_sym] ||= {}
1872
+ PWN::Env[:ai][:active] = engine.to_s
1873
+ PWN::Env[:ai][engine.to_sym][:model] = model unless model.to_s.strip.empty?
1874
+ persisted = persist_ai_selection(engine: engine, model: PWN::Env[:ai][engine.to_sym][:model])
1875
+ shown = PWN::Env[:ai][engine.to_sym][:model]
1876
+ msg = "active=#{engine} model=#{shown.to_s.empty? ? '(unset)' : shown}"
1877
+ msg = "#{msg} (session only)" unless persisted
1878
+ puts "[*] #{msg}"
1879
+ msg
1880
+ end
1881
+
1882
+ public_class_method def self.persist_ai_selection(opts = {})
1883
+ engine = opts[:engine].to_s
1884
+ model = opts[:model]
1885
+ return false if engine.empty?
1886
+
1887
+ env_path = nil
1888
+ dec_path = nil
1889
+ if defined?(PWN::Env) && PWN::Env.is_a?(Hash)
1890
+ env_path = PWN::Env.dig(:driver_opts, :pwn_env_path)
1891
+ dec_path = PWN::Env.dig(:driver_opts, :pwn_dec_path)
1892
+ end
1893
+ env_path = env_path.to_s.strip
1894
+ env_path = File.join(Dir.home, '.pwn', 'pwn.yaml') if env_path.empty?
1895
+ dec_path = dec_path.to_s.strip
1896
+ dec_path = "#{env_path}.decryptor" if dec_path.empty?
1897
+ return false unless File.exist?(env_path) && File.exist?(dec_path) && File.readable?(dec_path)
1898
+
1899
+ decryptor = YAML.load_file(dec_path, symbolize_names: true)
1900
+ key = decryptor.is_a?(Hash) ? decryptor[:key] : nil
1901
+ iv = decryptor.is_a?(Hash) ? decryptor[:iv] : nil
1902
+ return false if key.to_s.strip.empty? || iv.to_s.strip.empty?
1903
+
1904
+ PWN::Plugins::Vault.decrypt(file: env_path, key: key, iv: iv)
1905
+ begin
1906
+ cfg = YAML.load_file(env_path, symbolize_names: true)
1907
+ cfg = {} unless cfg.is_a?(Hash)
1908
+ cfg[:ai] = {} unless cfg[:ai].is_a?(Hash)
1909
+ cfg[:ai][:active] = engine
1910
+ unless model.to_s.strip.empty?
1911
+ slot = engine.to_sym
1912
+ cfg[:ai][slot] = {} unless cfg[:ai][slot].is_a?(Hash)
1913
+ cfg[:ai][slot][:model] = model
1914
+ end
1915
+ yaml_env = YAML.dump(cfg).gsub(/^(\s*):/, '\1')
1916
+ File.write(env_path, yaml_env)
1917
+ File.chmod(0o600, env_path)
1918
+ ensure
1919
+ PWN::Plugins::Vault.encrypt(file: env_path, key: key, iv: iv)
1920
+ end
1921
+ true
1922
+ rescue StandardError => e
1923
+ warn "[pwn-ai] /model persist skipped: #{e.class}: #{e.message}"
1924
+ false
1925
+ end
1926
+
1745
1927
  public_class_method def self.pwn_ai_run_cron(opts = {})
1746
1928
  args = Array(opts[:args])
1747
1929
  sub = args[0] || 'list'
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.696'
4
+ VERSION = '0.5.697'
5
5
  end
@@ -427,12 +427,13 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
427
427
  expect(src).not_to match(/dispatch #\{name\} ok_len=/)
428
428
  end
429
429
 
430
- it 'records a reconstruct-first timeout mistake instead of treating success:true as ok' do
430
+ it 'records a timeout increment mistake instead of treating success:true as ok' do
431
431
  tmp = Dir.mktmpdir
432
432
  stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
433
+ PWN::AI::Agent::ToolGuard.reset_timeout_budget! if PWN::AI::Agent::ToolGuard.respond_to?(:reset_timeout_budget!)
433
434
  raw = JSON.generate(
434
435
  success: true,
435
- result: { stdout: '', error: 'timeout after 20s', hint: 'x', scenario: 'construction' }
436
+ result: { stdout: '', error: 'timeout after 20s', hint: 'x', scenario: 'deadline' }
436
437
  )
437
438
  tele = described_class.send(
438
439
  :record_metrics,
@@ -443,7 +444,7 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
443
444
  )
444
445
  expect(tele[:ok]).to eq false
445
446
  expect(tele[:mistake]).to be_a(Hash)
446
- expect(tele[:mistake][:error].to_s).to match(/reconstruct/)
447
+ expect(tele[:mistake][:error].to_s).to match(/timeout \+= 180|too short/i)
447
448
  expect(tele[:mistake][:shape].to_s).to eq('timeout')
448
449
  end
449
450
 
@@ -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 'clamps an explicit timeout and derives a default when omitted' do
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: 9_999, kind: :eval)).to eq(90)
74
- expect(described_class.deadline_s(timeout: 9_999, kind: :shell)).to eq(180)
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
- it 'prefers reconstruct-the-payload before raising timeout' do
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(tool: 'pwn_eval', payload: 'sleep 3', timeout: 1)
85
- expect(first[:scenario]).to eq(:construction)
86
- expect(first[:hint]).to match(/reconstruct|generated differently|improperly/i)
87
- expect(first[:hint]).to match(/do not first raise timeout/i)
88
- expect(first[:error]).to match(/reconstruct/)
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 'switches to deadline-too-short only after a prior timeout mistake' do
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
- PWN::AI::Agent::Mistakes.record(
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
- error: 'shell timeout: reconstruct payload to same goal before raising timeout',
97
- args: 'sleep 3',
98
- source: :tool,
99
- shape: :timeout
160
+ payload: 'sleep 3',
161
+ timeout: 180,
162
+ task: 't1'
100
163
  )
101
- second = described_class.timeout_lesson(tool: 'shell', payload: 'sleep 3', timeout: 1)
102
- expect(second[:scenario]).to eq(:deadline)
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(/reconstruct|generated differently|improperly/i)
53
- expect(result[:scenario].to_s).to eq('construction')
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 prefers reconstruct-the-command before raising timeout' do
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(/reconstruct|generated differently|improperly/i)
69
- expect(r[:scenario].to_s).to eq('construction')
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
@@ -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
@@ -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
@@ -383,6 +383,7 @@
383
383
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.task_summary_flush! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.task_summary_flush!`: "}]}
384
384
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.task_summary_plan! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.task_summary_plan!`: "}]}
385
385
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.task_summary_record! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.task_summary_record!`: "}]}
386
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.timeout_task Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.timeout_task`: "}]}
386
387
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.tool_effects Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.tool_effects`: "}]}
387
388
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.world_knowledge? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.world_knowledge?`: "}]}
388
389
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_verified? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_verified?`: "}]}
@@ -706,18 +707,30 @@
706
707
  {"messages":[{"role":"user","content":"PWN::AI::Agent::TaskSummarizer.why_bit Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TaskSummarizer.why_bit`: "}]}
707
708
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.authors`: "}]}
708
709
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.bashism? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.bashism?`: "}]}
710
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.budget_exhausted? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.budget_exhausted?`: "}]}
709
711
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.coerce_args Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.coerce_args`: "}]}
710
712
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.deadline_s Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.deadline_s`: "}]}
711
713
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.help`: "}]}
712
714
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.host_load Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.host_load`: "}]}
713
715
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.invalid_payload Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.invalid_payload`: "}]}
716
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.mutation_count Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.mutation_count`: "}]}
717
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.next_timeout Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.next_timeout`: "}]}
718
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.note_timeout! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.note_timeout!`: "}]}
719
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.payload_key Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.payload_key`: "}]}
720
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.payload_spent Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.payload_spent`: "}]}
714
721
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.placeholder? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.placeholder?`: "}]}
715
722
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.present? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.present?`: "}]}
723
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.reset_timeout_budget Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.reset_timeout_budget`: "}]}
724
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.reset_timeout_budget! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.reset_timeout_budget!`: "}]}
716
725
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.shell_bash? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.shell_bash?`: "}]}
717
726
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.shell_name Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.shell_name`: "}]}
727
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.task_key Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.task_key`: "}]}
718
728
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_lesson Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_lesson`: "}]}
729
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_mutated Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_mutated`: "}]}
730
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_mutations Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_mutations`: "}]}
719
731
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_prior_count Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_prior_count`: "}]}
720
732
  {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_result Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_result`: "}]}
733
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::ToolGuard.timeout_spent Usage"},{"role":"assistant","content":"`PWN::AI::Agent::ToolGuard.timeout_spent`: "}]}
721
734
  {"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
735
  {"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
736
  {"messages":[{"role":"user","content":"PWN::AI::Agent::TransparentBrowser.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::TransparentBrowser.help`: "}]}
@@ -2140,11 +2153,13 @@
2140
2153
  {"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
2154
  {"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
2155
  {"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"}]}
2156
+ {"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.apply_port_scan_opts Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.apply_port_scan_opts`: "}]}
2143
2157
  {"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
2158
  {"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
2159
  {"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.help Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.help`: "}]}
2160
+ {"messages":[{"role":"user","content":"PWN::Plugins::NmapIt.nmap_compat Usage"},{"role":"assistant","content":"`PWN::Plugins::NmapIt.nmap_compat`: "}]}
2146
2161
  {"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"}]}
2162
+ {"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
2163
  {"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
2164
  {"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
2165
  {"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 +2243,21 @@
2228
2243
  {"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
2244
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.help Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.help`: "}]}
2230
2245
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.install_pwn_ai_completer! Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.install_pwn_ai_completer!`: "}]}
2246
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL.persist_ai_selection Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.persist_ai_selection`: "}]}
2231
2247
  {"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
2248
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_complete_command Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_complete_command`: "}]}
2233
2249
  {"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
2250
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_complete_path Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_complete_path`: "}]}
2235
2251
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_complete_ruby Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_complete_ruby`: "}]}
2236
2252
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_dispatch_slash! Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_dispatch_slash!`: "}]}
2253
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_engine_model Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_engine_model`: "}]}
2254
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_engines Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_engines`: "}]}
2255
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_list_llms Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_list_llms`: "}]}
2256
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_model_ids Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_model_ids`: "}]}
2257
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_provider_class Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_provider_class`: "}]}
2237
2258
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_run_cron Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_run_cron`: "}]}
2238
2259
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_run_memory Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_run_memory`: "}]}
2260
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_run_model Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_run_model`: "}]}
2239
2261
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_run_sessions Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_run_sessions`: "}]}
2240
2262
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.pwn_ai_run_skills Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.pwn_ai_run_skills`: "}]}
2241
2263
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.ready_tty! Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.ready_tty!`: "}]}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pwn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.696
4
+ version: 0.5.697
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.