pwn 0.5.749 → 0.5.751
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/documentation/Blockchain.md +107 -9
- data/documentation/pwn-ai-Agent.md +310 -4
- data/documentation/pwn_silent_help_learn_demo.gif +0 -0
- data/etc/default_skills/pwn/banner/SKILL.md +7 -3
- data/etc/default_skills/pwn/blockchain/btc/SKILL.md +8 -5
- data/etc/default_skills/pwn/blockchain/eth/SKILL.md +12 -3
- data/etc/default_skills/pwn/blockchain/eth/references/urls.md +1 -1
- data/etc/default_skills/pwn/plugins/repl/SKILL.md +5 -0
- data/etc/default_skills/pwn/plugins/repl/ai/SKILL.md +4 -0
- data/etc/default_skills/pwn/plugins/repl/ai/references/urls.md +5 -0
- data/etc/default_skills/pwn/plugins/repl/ai_console/SKILL.md +48 -0
- data/etc/default_skills/pwn/plugins/repl/ai_console_commands/SKILL.md +47 -0
- data/etc/default_skills/pwn/plugins/repl/ai_console_usage/SKILL.md +48 -0
- data/etc/default_skills/pwn/plugins/repl/ai_swarm/SKILL.md +46 -0
- data/lib/pwn/ai/agent/loop.rb +287 -18
- data/lib/pwn/ai/agent/swarm.rb +28 -4
- data/lib/pwn/ai/open_ai.rb +4 -2
- data/lib/pwn/banner.rb +743 -2
- data/lib/pwn/blockchain/btc.rb +494 -196
- data/lib/pwn/blockchain/eth.rb +440 -81
- data/lib/pwn/blockchain.rb +6 -0
- data/lib/pwn/config.rb +22 -0
- data/lib/pwn/migrate.rb +9 -1
- data/lib/pwn/plugins/repl/ai.rb +182 -17
- data/lib/pwn/plugins/repl/ai_console.rb +1581 -0
- data/lib/pwn/plugins/repl/ai_console_commands.rb +282 -0
- data/lib/pwn/plugins/repl/ai_console_usage.rb +144 -0
- data/lib/pwn/plugins/repl/ai_swarm.rb +375 -0
- data/lib/pwn/plugins/repl.rb +25 -3
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +258 -1
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +3 -2
- data/spec/lib/pwn/ai/open_ai_spec.rb +5 -0
- data/spec/lib/pwn/banner_spec.rb +643 -0
- data/spec/lib/pwn/blockchain/btc_spec.rb +372 -1
- data/spec/lib/pwn/blockchain/eth_spec.rb +205 -0
- data/spec/lib/pwn/blockchain_spec.rb +6 -0
- data/spec/lib/pwn/config_spec.rb +10 -0
- data/spec/lib/pwn/migrate_spec.rb +40 -3
- data/spec/lib/pwn/plugins/repl/ai_console_commands_spec.rb +65 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +1434 -0
- data/spec/lib/pwn/plugins/repl/ai_console_usage_spec.rb +29 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +127 -0
- data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +5 -3
- data/spec/lib/pwn/plugins/repl_spec.rb +167 -1
- data/third_party/pwn_rdoc.jsonl +229 -10
- metadata +18 -6
- data/etc/default_skills/pwn/blockchain/btc/references/urls.md +0 -3
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
module PWN
|
|
8
|
+
module Plugins
|
|
9
|
+
module REPL
|
|
10
|
+
# Console-owned controller over the existing Swarm persona APIs.
|
|
11
|
+
module AISwarm
|
|
12
|
+
# One cancellable persona job. Model waits can be interrupted; tools finish.
|
|
13
|
+
class JobControl < PWN::AI::Agent::Loop::Steering
|
|
14
|
+
class Stopped < Exception; end # rubocop:disable Lint/InheritException
|
|
15
|
+
|
|
16
|
+
def stop
|
|
17
|
+
@stopping = true
|
|
18
|
+
submit('Stop this swarm job.')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def checkpoint(messages:, phase: :boundary, &block)
|
|
22
|
+
if @stopping
|
|
23
|
+
block&.call
|
|
24
|
+
raise Stopped, 'Swarm job cancelled at a safe boundary; completed work is not undone.'
|
|
25
|
+
end
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def check_stop
|
|
30
|
+
raise Stopped, 'Swarm job cancelled at a safe boundary; completed work is not undone.' if @stopping
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Session-scoped swarm jobs. Workers never draw the terminal.
|
|
35
|
+
class Controller # rubocop:disable Metrics/ClassLength -- one session-scoped swarm command surface
|
|
36
|
+
MAX_JOBS = 8
|
|
37
|
+
ACTIVE = %w[queued running steering cancelling].freeze
|
|
38
|
+
|
|
39
|
+
def initialize(session_id:)
|
|
40
|
+
@session_id = session_id.to_s
|
|
41
|
+
@mutex = Mutex.new
|
|
42
|
+
@jobs = {}
|
|
43
|
+
@swarm_id = nil
|
|
44
|
+
@closed = false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def execute(opts = {})
|
|
48
|
+
line = opts[:line].to_s.strip
|
|
49
|
+
tokens = Shellwords.split(line)
|
|
50
|
+
tokens.shift if tokens.first == '/swarm'
|
|
51
|
+
command = tokens.shift || 'help'
|
|
52
|
+
dispatch(command, tokens, opts)
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
{ ok: false, error: "#{e.class}: #{e.message}" }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def snapshot
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
{
|
|
60
|
+
swarm_id: @swarm_id,
|
|
61
|
+
jobs: @jobs.values.map { |job| job.slice(:id, :agent, :state, :command, :result, :error, :last_tool).transform_values { |value| value.is_a?(String) ? value.dup : value } }
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def completion_context
|
|
67
|
+
snap = snapshot
|
|
68
|
+
{ swarm_id: snap[:swarm_id], jobs: snap[:jobs], agents: agents }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def busy?
|
|
72
|
+
snapshot[:jobs].any? { |job| ACTIVE.include?(job[:state]) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def roster
|
|
76
|
+
PWN::AI::Agent::Swarm.personas(swarm_id: @swarm_id).map do |name, persona|
|
|
77
|
+
{ name: name.to_s, role: persona[:role].to_s, engine: (persona[:engine] || 'inherit').to_s, model: (persona[:model] || 'provider default').to_s }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def cancel_all
|
|
82
|
+
ids = snapshot[:jobs].select { |job| ACTIVE.include?(job[:state]) }.map { |job| job[:id] }
|
|
83
|
+
ids.each { |id| cancel(id) }
|
|
84
|
+
{ ok: true, cancelled: ids.length }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def close
|
|
88
|
+
@closed = true
|
|
89
|
+
cancel_all
|
|
90
|
+
workers.each { |worker| worker.join(30) }
|
|
91
|
+
{ ok: true }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def dispatch(command, tokens, opts)
|
|
97
|
+
case command
|
|
98
|
+
when 'help', 'dashboard' then help_text
|
|
99
|
+
when 'roster' then { ok: true, agents: roster, swarm_id: @swarm_id }
|
|
100
|
+
when 'status' then status(tokens.first)
|
|
101
|
+
when 'create' then create(tokens.join(' '))
|
|
102
|
+
when 'use' then use(tokens.first)
|
|
103
|
+
when 'spawn' then spawn(tokens)
|
|
104
|
+
when 'retire' then retire(tokens.first)
|
|
105
|
+
when 'ask' then ask(tokens, opts)
|
|
106
|
+
when 'debate' then debate(tokens, opts)
|
|
107
|
+
when 'broadcast' then broadcast(tokens, opts)
|
|
108
|
+
when 'tail' then tail(tokens.first)
|
|
109
|
+
when 'steer' then steer(tokens, opts)
|
|
110
|
+
when 'cancel' then cancel(tokens.first)
|
|
111
|
+
else { ok: false, error: "unknown /swarm command: #{command}", hint: help_text[:usage] }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def help_text
|
|
116
|
+
{
|
|
117
|
+
ok: true,
|
|
118
|
+
usage: '/swarm roster|status [JOB]|create [topic]|use ID|spawn NAME ROLE [--engine E --model M --toolsets a,b]|retire NAME|ask NAME REQUEST|debate NAMES TOPIC [--rounds N]|broadcast REQUEST [--names a,b]|tail [N]|steer JOB INSTRUCTION|cancel JOB|all'
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def create(topic)
|
|
123
|
+
return { ok: false, error: 'cannot create a swarm while jobs are active' } if busy?
|
|
124
|
+
|
|
125
|
+
created = PWN::AI::Agent::Swarm.create(topic: topic.empty? ? @session_id : topic)
|
|
126
|
+
@mutex.synchronize { @swarm_id = created[:swarm_id] }
|
|
127
|
+
{ ok: true, swarm_id: created[:swarm_id] }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def use(swarm_id)
|
|
131
|
+
return { ok: false, error: 'swarm id required' } unless identifier?(swarm_id)
|
|
132
|
+
return { ok: false, error: 'cannot switch swarm while jobs are active' } if busy?
|
|
133
|
+
|
|
134
|
+
@mutex.synchronize { @swarm_id = swarm_id }
|
|
135
|
+
{ ok: true, swarm_id: swarm_id }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def spawn(tokens)
|
|
139
|
+
flags, positional = split_flags(tokens)
|
|
140
|
+
name = positional[0]
|
|
141
|
+
role = positional[1..]&.join(' ')
|
|
142
|
+
return { ok: false, error: 'usage: spawn NAME ROLE [--engine E --model M --toolsets a,b]' } if name.to_s.empty? || role.to_s.empty?
|
|
143
|
+
return { ok: false, error: 'invalid persona name' } unless identifier?(name)
|
|
144
|
+
|
|
145
|
+
ensure_swarm
|
|
146
|
+
result = PWN::AI::Agent::Swarm.spawn(
|
|
147
|
+
swarm_id: @swarm_id, name: name, role: role, engine: flags['engine'], model: flags['model'],
|
|
148
|
+
toolsets: flags['toolsets']&.split(','), ephemeral: true
|
|
149
|
+
)
|
|
150
|
+
{ ok: true, persona: result[:name], swarm_id: @swarm_id, scope: :swarm }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def retire(name)
|
|
154
|
+
return { ok: false, error: 'invalid persona name' } unless identifier?(name)
|
|
155
|
+
|
|
156
|
+
result = PWN::AI::Agent::Swarm.retire(name: name, swarm_id: @swarm_id, local: true)
|
|
157
|
+
{ ok: result[:removed], scope: result[:scope], name: name }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def ask(tokens, opts)
|
|
161
|
+
name = tokens.shift
|
|
162
|
+
request = tokens.join(' ')
|
|
163
|
+
return { ok: false, error: 'usage: ask NAME REQUEST' } if name.to_s.empty? || request.empty?
|
|
164
|
+
|
|
165
|
+
start_job(agent: name, command: 'ask', opts: opts) do |job|
|
|
166
|
+
PWN::AI::Agent::Swarm.ask(name: name, request: request, swarm_id: ensure_swarm, steering: job[:control], usage_observer: opts[:usage_observer], on_tool: tool_callback(job, opts))
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def debate(tokens, opts)
|
|
171
|
+
flags, positional = split_flags(tokens)
|
|
172
|
+
names = positional.shift.to_s.split(',')
|
|
173
|
+
topic = positional.join(' ')
|
|
174
|
+
rounds = (flags['rounds'] || 1).to_i
|
|
175
|
+
return { ok: false, error: 'usage: debate NAME,NAME TOPIC [--rounds N]' } if names.length < 2 || topic.empty?
|
|
176
|
+
return { ok: false, error: 'rounds must be between 1 and 10' } unless rounds.between?(1, 10)
|
|
177
|
+
|
|
178
|
+
start_job(agent: names.join(','), command: 'debate', opts: opts) do |job|
|
|
179
|
+
transcript = []
|
|
180
|
+
rounds.times do
|
|
181
|
+
names.each do |name|
|
|
182
|
+
request = transcript.empty? ? topic : "#{topic}\n\nPrevious speaker: #{transcript.last}\nRespond, critique, or advance the objective."
|
|
183
|
+
job[:control].check_stop
|
|
184
|
+
result = PWN::AI::Agent::Swarm.ask(name: name, request: request, swarm_id: @swarm_id, steering: job[:control], usage_observer: opts[:usage_observer], on_tool: tool_callback(job, opts))
|
|
185
|
+
transcript << "#{name}: #{result[:reply]}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
{ reply: transcript.join("\n") }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def broadcast(tokens, opts)
|
|
193
|
+
flags, positional = split_flags(tokens)
|
|
194
|
+
request = positional.join(' ')
|
|
195
|
+
names = flags['names']&.split(',') || agents
|
|
196
|
+
return { ok: false, error: 'usage: broadcast REQUEST [--names a,b]' } if request.empty? || names.empty?
|
|
197
|
+
|
|
198
|
+
start_job(agent: names.join(','), command: 'broadcast', opts: opts) do |job|
|
|
199
|
+
replies = names.map do |name|
|
|
200
|
+
job[:control].check_stop
|
|
201
|
+
result = PWN::AI::Agent::Swarm.ask(name: name, request: request, swarm_id: ensure_swarm, steering: job[:control], usage_observer: opts[:usage_observer], on_tool: tool_callback(job, opts))
|
|
202
|
+
"#{name}: #{result[:reply]}"
|
|
203
|
+
end
|
|
204
|
+
{ reply: replies.join("\n") }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def steer(tokens, opts)
|
|
209
|
+
id = tokens.shift
|
|
210
|
+
instruction = tokens.join(' ')
|
|
211
|
+
job = find_job(id)
|
|
212
|
+
return { ok: false, error: 'job not found' } unless job
|
|
213
|
+
return { ok: false, error: 'instruction required' } if instruction.empty?
|
|
214
|
+
return { ok: false, error: 'job is no longer active' } unless update_active(job, 'steering')
|
|
215
|
+
|
|
216
|
+
job[:control].submit(instruction)
|
|
217
|
+
opts[:on_event]&.call(:notice, "Steering #{job[:id]} (#{job[:agent]}): waiting for a safe boundary.")
|
|
218
|
+
{ ok: true, job_id: job[:id] }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def cancel(id)
|
|
222
|
+
return cancel_all if id.nil? || id == 'all'
|
|
223
|
+
|
|
224
|
+
job = find_job(id)
|
|
225
|
+
return { ok: false, error: 'job not found' } unless job
|
|
226
|
+
|
|
227
|
+
return { ok: false, error: 'job is no longer active' } unless update_active(job, 'cancelling')
|
|
228
|
+
|
|
229
|
+
job[:control].stop
|
|
230
|
+
{ ok: true, job_id: job[:id] }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def status(id)
|
|
234
|
+
jobs = snapshot[:jobs]
|
|
235
|
+
jobs = jobs.select { |job| job[:id] == id } if id
|
|
236
|
+
{ ok: true, swarm_id: @swarm_id, jobs: jobs }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def tail(limit)
|
|
240
|
+
return { ok: false, error: 'no swarm selected' } if @swarm_id.to_s.empty?
|
|
241
|
+
|
|
242
|
+
{ ok: true, messages: PWN::AI::Agent::Swarm.bus_tail(swarm_id: @swarm_id, limit: (limit || 8).to_i) }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def start_job(agent:, command:, opts:)
|
|
246
|
+
return { ok: false, error: 'swarm controller is closed' } if @closed
|
|
247
|
+
return { ok: false, error: "job limit #{MAX_JOBS} reached" } if busy_count >= MAX_JOBS
|
|
248
|
+
|
|
249
|
+
ensure_swarm
|
|
250
|
+
id = SecureRandom.hex(3)
|
|
251
|
+
locals = Thread.current.keys.grep(/^pwn_/).to_h { |key| [key, Thread.current[key]] }
|
|
252
|
+
variables = Thread.current.thread_variables.grep(/^pwn_/).to_h { |key| [key, Thread.current.thread_variable_get(key)] }
|
|
253
|
+
ready = Queue.new
|
|
254
|
+
job = { id: id, agent: agent, command: command, state: 'queued' }
|
|
255
|
+
@mutex.synchronize { @jobs[id] = job }
|
|
256
|
+
job[:worker] = Thread.new do
|
|
257
|
+
locals.each { |key, value| Thread.current[key] = value }
|
|
258
|
+
variables.each { |key, value| Thread.current.thread_variable_set(key, value) }
|
|
259
|
+
job[:control] = JobControl.new(input: StringIO.new, output: StringIO.new)
|
|
260
|
+
Thread.current[:pwn_steering_input] = job[:control]
|
|
261
|
+
ready << true
|
|
262
|
+
update(job, 'running')
|
|
263
|
+
result = yield(job)
|
|
264
|
+
job[:control].check_stop
|
|
265
|
+
update(job, 'completed', result: summarize(result))
|
|
266
|
+
opts[:on_event]&.call(:assistant, "#{agent} [#{id}] #{command} complete\n#{summarize(result)}")
|
|
267
|
+
result
|
|
268
|
+
rescue JobControl::Stopped => e
|
|
269
|
+
update(job, 'cancelled', error: e.message)
|
|
270
|
+
opts[:on_event]&.call(:warning, "#{agent} [#{id}] #{e.message}")
|
|
271
|
+
rescue StandardError => e
|
|
272
|
+
update(job, 'failed', error: "#{e.class}: #{e.message}")
|
|
273
|
+
opts[:on_event]&.call(:warning, "#{agent} [#{id}] #{e.class}: #{e.message}")
|
|
274
|
+
end
|
|
275
|
+
job[:worker].report_on_exception = false
|
|
276
|
+
ready.pop
|
|
277
|
+
{ ok: true, job_id: id, state: 'queued' }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def tool_callback(job, opts)
|
|
281
|
+
lambda do |name, args, result|
|
|
282
|
+
update(job, job[:state], last_tool: name)
|
|
283
|
+
opts[:on_event]&.call(:tool, "#{job[:agent]} [#{job[:id]}] #{name}\n#{args.inspect}")
|
|
284
|
+
opts[:on_event]&.call(:result, result.to_s)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def summarize(result)
|
|
289
|
+
return result[:reply].to_s if result.is_a?(Hash) && result[:reply]
|
|
290
|
+
return result[:transcript].map { |row| "#{row[:name]}: #{row[:reply]}" }.join("\n") if result.is_a?(Hash) && result[:transcript]
|
|
291
|
+
|
|
292
|
+
result.to_s
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def update(job, state, **outcome)
|
|
296
|
+
clean = outcome.transform_values { |value| PWN::Redaction.redact(value: value.to_s) }
|
|
297
|
+
@mutex.synchronize { job.merge!(clean).merge!(state: state) }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def update_active(job, state)
|
|
301
|
+
@mutex.synchronize do
|
|
302
|
+
return false unless ACTIVE.include?(job[:state])
|
|
303
|
+
|
|
304
|
+
job[:state] = state
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def find_job(id)
|
|
309
|
+
@mutex.synchronize { @jobs[id] }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def controls
|
|
313
|
+
@mutex.synchronize { @jobs.values.map { |job| job[:control] } }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def workers
|
|
317
|
+
@mutex.synchronize { @jobs.values.filter_map { |job| job[:worker] } }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def busy_count
|
|
321
|
+
snapshot[:jobs].count { |job| ACTIVE.include?(job[:state]) }
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def ensure_swarm
|
|
325
|
+
@swarm_id || create('console')[:swarm_id]
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def agents
|
|
329
|
+
roster.map { |row| row[:name] }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def identifier?(value)
|
|
333
|
+
value.to_s.match?(/\A[A-Za-z0-9][A-Za-z0-9_.-]{0,63}\z/)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def split_flags(tokens)
|
|
337
|
+
flags = {}
|
|
338
|
+
positional = []
|
|
339
|
+
index = 0
|
|
340
|
+
while index < tokens.length
|
|
341
|
+
token = tokens[index]
|
|
342
|
+
if token == '--'
|
|
343
|
+
positional.concat(tokens[(index + 1)..])
|
|
344
|
+
break
|
|
345
|
+
elsif %w[--engine --model --toolsets --names --rounds].include?(token)
|
|
346
|
+
flags[token.delete_prefix('--')] = tokens[index + 1].to_s
|
|
347
|
+
index += 2
|
|
348
|
+
else
|
|
349
|
+
positional << token
|
|
350
|
+
index += 1
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
[flags, positional]
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
public_class_method def self.authors
|
|
358
|
+
'AUTHOR(S): 0day Inc. <support@0dayinc.com>'
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
public_class_method def self.help
|
|
362
|
+
puts "USAGE:
|
|
363
|
+
# Build a session-scoped controller. Jobs call the existing Swarm APIs.
|
|
364
|
+
controller = #{self}::Controller.new(
|
|
365
|
+
session_id: 'required - console session id'
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Return module authors.
|
|
369
|
+
#{self}.authors
|
|
370
|
+
"
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -16,6 +16,10 @@ module PWN
|
|
|
16
16
|
module REPL
|
|
17
17
|
autoload :ASM, 'pwn/plugins/repl/asm'
|
|
18
18
|
autoload :AI, 'pwn/plugins/repl/ai'
|
|
19
|
+
autoload :AIConsole, 'pwn/plugins/repl/ai_console'
|
|
20
|
+
autoload :AIConsoleCommands, 'pwn/plugins/repl/ai_console_commands'
|
|
21
|
+
autoload :AIConsoleUsage, 'pwn/plugins/repl/ai_console_usage'
|
|
22
|
+
autoload :AISwarm, 'pwn/plugins/repl/ai_swarm'
|
|
19
23
|
autoload :Mesh, 'pwn/plugins/repl/mesh'
|
|
20
24
|
autoload :Vault, 'pwn/plugins/repl/vault'
|
|
21
25
|
|
|
@@ -579,7 +583,7 @@ module PWN
|
|
|
579
583
|
puts
|
|
580
584
|
mirror.call("#{header}\n#{argv_out}#{res_header}\n#{res_out}\n")
|
|
581
585
|
end
|
|
582
|
-
final = PWN::
|
|
586
|
+
final = PWN::Plugins::REPL.pwn_ai_run_steerable(
|
|
583
587
|
request: orig_request,
|
|
584
588
|
session_id: sess_id,
|
|
585
589
|
enabled_toolsets: PWN::Env.dig(:ai, :agent, :toolsets),
|
|
@@ -1104,13 +1108,31 @@ module PWN
|
|
|
1104
1108
|
|
|
1105
1109
|
# Run pwn ai run model and return its result
|
|
1106
1110
|
#{self}.pwn_ai_run_model(
|
|
1107
|
-
args: 'optional - Array args value consumed by #pwn_ai_run_model'
|
|
1111
|
+
args: 'optional - Array args value consumed by #pwn_ai_run_model',
|
|
1112
|
+
prompt: 'optional - callable receiving selection metadata; return effort, nil to cancel, or :deferred'
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
# Resolve provider-supported effort options for a submitted model selection.
|
|
1116
|
+
#{self}.pwn_ai_reasoning_selection(
|
|
1117
|
+
engine: 'required - provider name',
|
|
1118
|
+
model: 'required - exact model identifier'
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
# Prompt for reasoning effort in the legacy line interface.
|
|
1122
|
+
#{self}.pwn_ai_prompt_reasoning(
|
|
1123
|
+
selection: 'required - model selection with efforts and default'
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
# Apply accepted engine, model and reasoning effort as one selection.
|
|
1127
|
+
#{self}.pwn_ai_apply_model(
|
|
1128
|
+
selection: 'required - engine, model and optional reasoning_effort'
|
|
1108
1129
|
)
|
|
1109
1130
|
|
|
1110
1131
|
# Run persist ai selection and return its result
|
|
1111
1132
|
#{self}.persist_ai_selection(
|
|
1112
1133
|
engine: 'required - engine value consumed by #persist_ai_selection',
|
|
1113
|
-
model: 'required - model value consumed by #persist_ai_selection'
|
|
1134
|
+
model: 'required - model value consumed by #persist_ai_selection',
|
|
1135
|
+
reasoning_effort: 'optional - accepted reasoning effort to merge into this engine'
|
|
1114
1136
|
)
|
|
1115
1137
|
|
|
1116
1138
|
# Run pwn ai run cron and return its result
|
data/lib/pwn/version.rb
CHANGED