pwn 0.5.783 → 0.5.785
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/.ruby-version +1 -1
- data/Gemfile +8 -8
- data/README.md +37 -0
- data/Rakefile +19 -1
- data/bin/pwn +2 -1
- data/bin/pwn_setup +1 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/build_gem.sh +1 -1
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +82 -0
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +64 -35
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +7 -6
- data/documentation/Installation.md +254 -10
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +17 -0
- data/documentation/Persistence.md +8 -6
- data/documentation/Plugin-Degradation.md +12 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Reinforcement-Learning.md +5 -6
- data/documentation/Runtime-Source-Protection.md +162 -0
- data/documentation/Transparent-Browser.md +11 -0
- data/documentation/Troubleshooting.md +3 -2
- data/documentation/What-is-PWN.md +2 -2
- data/documentation/YubiKey-Vaults.md +183 -0
- data/documentation/YubiKey.md +339 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -128
- data/documentation/diagrams/dot/cron-scheduling.dot +5 -5
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +1245 -22
- data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
- data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
- data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
- data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
- data/etc/default_skills/pwn/backups/SKILL.md +54 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
- data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
- data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
- data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
- data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
- data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
- data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
- data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
- data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
- data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
- data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
- data/lib/pwn/ai/agent/dispatch.rb +26 -15
- data/lib/pwn/ai/agent/extrospection.rb +6 -7
- data/lib/pwn/ai/agent/learning.rb +62 -25
- data/lib/pwn/ai/agent/loop.rb +17 -1
- data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
- data/lib/pwn/ai/agent/policy.rb +5 -0
- data/lib/pwn/ai/agent/policy_evaluation.rb +8 -8
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
- data/lib/pwn/ai/agent/request_runtime.rb +122 -18
- data/lib/pwn/ai/agent/reward.rb +5 -3
- data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
- data/lib/pwn/ai/agent/skill_review.rb +9 -5
- data/lib/pwn/ai/agent/swarm.rb +35 -16
- data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
- data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
- data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
- data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/ai/anthropic.rb +8 -33
- data/lib/pwn/ai/cli.rb +34 -5
- data/lib/pwn/ai/gemini.rb +1 -0
- data/lib/pwn/ai/grok.rb +39 -28
- data/lib/pwn/ai/ollama.rb +4 -2
- data/lib/pwn/ai/open_ai.rb +11 -32
- data/lib/pwn/ai/open_web_ui.rb +62 -4
- data/lib/pwn/ai/router.rb +53 -3
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +347 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/cron.rb +120 -49
- data/lib/pwn/migrate.rb +150 -68
- data/lib/pwn/module_skills.rb +7 -0
- data/lib/pwn/plugins/ai_sandbox.rb +538 -1
- data/lib/pwn/plugins/authentication_helper.rb +74 -4
- data/lib/pwn/plugins/basic_auth.rb +2 -1
- data/lib/pwn/plugins/binary_parser.rb +5 -4
- data/lib/pwn/plugins/ghidra.rb +4 -3
- data/lib/pwn/plugins/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +21 -3
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +305 -27
- data/lib/pwn/plugins/repl/ai_console.rb +273 -89
- data/lib/pwn/plugins/repl/ai_swarm.rb +181 -62
- data/lib/pwn/plugins/repl/mesh.rb +3 -16
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +1 -216
- data/lib/pwn/plugins/sandbox/driver.rb +4 -0
- data/lib/pwn/plugins/transparent_browser.rb +215 -0
- data/lib/pwn/plugins/vault.rb +412 -135
- data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
- data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
- data/lib/pwn/plugins/yubikey/management.rb +686 -0
- data/lib/pwn/plugins/yubikey/oath.rb +766 -0
- data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
- data/lib/pwn/plugins/yubikey/otp.rb +897 -0
- data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
- data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
- data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
- data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
- data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
- data/lib/pwn/plugins/yubikey.rb +844 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/redaction.rb +69 -12
- data/lib/pwn/session_trace.rb +4 -0
- data/lib/pwn/sessions.rb +58 -16
- data/lib/pwn/setup.rb +123 -41
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/app_cobalt_io.rb +16 -5
- data/lib/pwn/www/bug_crowd.rb +16 -5
- data/lib/pwn/www/coinbase_pro.rb +16 -5
- data/lib/pwn/www/github.rb +30 -14
- data/lib/pwn/www/paypal.rb +15 -4
- data/lib/pwn/www/synack.rb +16 -5
- data/lib/pwn/www/twitter.rb +17 -6
- data/lib/pwn.rb +1 -0
- data/scripts/build_gate.rb +58 -0
- data/spec/documentation/installation_md_spec.rb +1 -1
- data/spec/fixtures/yubikey/apdu_frames.json +162 -0
- data/spec/integration/authentication_helper_oath_spec.rb +117 -0
- data/spec/integration/build_gate_spec.rb +73 -0
- data/spec/integration/controller_spill_spec.rb +48 -0
- data/spec/integration/harness_process_spec.rb +97 -0
- data/spec/integration/mission_cancel_spec.rb +79 -0
- data/spec/integration/mission_curses_pty_spec.rb +44 -0
- data/spec/integration/persistence_roundtrip_spec.rb +1 -7
- data/spec/integration/readme_security_spec.rb +47 -0
- data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
- data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
- data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
- data/spec/lib/pwn/ai/agent/policy_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
- data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
- data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
- data/spec/lib/pwn/ai/cli_spec.rb +6 -9
- data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
- data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
- data/spec/lib/pwn/ai/router_spec.rb +31 -3
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +114 -0
- data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
- data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
- data/spec/lib/pwn/cron_spec.rb +113 -6
- data/spec/lib/pwn/migrate_spec.rb +108 -5
- data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/module_skills_spec.rb +8 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
- data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
- data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
- data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
- data/spec/lib/pwn/plugins/log_spec.rb +7 -0
- data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
- data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
- data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +125 -78
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +30 -16
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
- data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
- data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
- data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
- data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
- data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
- data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
- data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
- data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
- data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
- data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
- data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
- data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
- data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
- data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
- data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
- data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
- data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
- data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
- data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
- data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
- data/spec/lib/pwn/redaction_spec.rb +13 -10
- data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_concurrency_spec.rb +201 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +167 -2
- data/spec/lib/pwn/session_cleanup_legacy_spec.rb +101 -0
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +335 -2
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +403 -0
- data/spec/lib/pwn/sessions_spec.rb +112 -0
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- data/spec/lib/pwn/setup_spec.rb +3 -1
- data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/gate_failure_formatter.rb +14 -0
- data/spec/support/harness/cli_faults.rb +13 -9
- data/spec/support/harness/controller_spill.rb +26 -0
- data/spec/support/harness/controller_state.rb +54 -0
- data/spec/support/harness/evaluate.rb +12 -1
- data/spec/support/harness/experiments.rb +1 -1
- data/spec/support/harness/fake_provider.rb +22 -6
- data/spec/support/harness/fault_preload.rb +3 -1
- data/spec/support/harness/mission_console.rb +54 -0
- data/spec/support/harness/mission_curses_pty.rb +41 -0
- data/spec/support/harness/mission_dispatch.rb +30 -0
- data/spec/support/harness/mission_fixture_preload.rb +24 -0
- data/spec/support/harness/mission_job_lifetime.rb +25 -0
- data/spec/support/harness/mission_legacy.rb +25 -0
- data/spec/support/harness/mission_normal_exit.rb +34 -0
- data/spec/support/harness/mission_swarm.rb +23 -0
- data/spec/support/harness/mission_unsupported.rb +29 -0
- data/spec/support/harness/package_acceptance.rb +33 -5
- data/spec/support/harness/session_acceptance.rb +4 -4
- data/spec/support/harness/swarm_acceptance.rb +1 -1
- data/spec/support/harness/swarm_control.rb +44 -0
- data/spec/support/mission_namespace.rb +14 -0
- data/spec/support/mission_pipe_queue.rb +33 -0
- data/third_party/pwn_rdoc.jsonl +599 -15
- metadata +140 -17
data/lib/pwn/plugins/repl/ai.rb
CHANGED
|
@@ -250,7 +250,7 @@ module PWN
|
|
|
250
250
|
end
|
|
251
251
|
end
|
|
252
252
|
PWN_AI_SLASH_COMMANDS = %w[
|
|
253
|
-
/back /cron /debug /delegate /help /learning /memory /mcp /model /sessions /skills /steer /swarm /system-role /trace /vault
|
|
253
|
+
/back /cron /debug /delegate /help /learning /maintenance /memory /mcp /model /sessions /skills /steer /swarm /system-role /trace /vault
|
|
254
254
|
].freeze
|
|
255
255
|
|
|
256
256
|
PWN_AI_SLASH_SUBCOMMANDS = {
|
|
@@ -265,6 +265,7 @@ module PWN
|
|
|
265
265
|
'/model' => %w[list],
|
|
266
266
|
'/swarm' => %w[mission agents status create use spawn retire dm debate broadcast tail steer pause resume cancel help],
|
|
267
267
|
'/sessions' => %w[list resume delete stats cleanup],
|
|
268
|
+
'/maintenance' => %w[status run dry-run],
|
|
268
269
|
|
|
269
270
|
'/skills' => %w[list recall],
|
|
270
271
|
'/learning' => %w[list requeue]
|
|
@@ -277,9 +278,294 @@ module PWN
|
|
|
277
278
|
# 2. '/' anywhere else → :path (host-native file nav)
|
|
278
279
|
# 3. else → :ruby (same Pry::InputCompleter menu as the pwn REPL)
|
|
279
280
|
class << PWN::Plugins::REPL # rubocop:disable Metrics/ClassLength
|
|
280
|
-
#
|
|
281
|
-
# readline and not a worker thread running tools without their locals.
|
|
281
|
+
# The terminal owner survives CTRL+C; only its mission tree is killed.
|
|
282
282
|
def pwn_ai_run_steerable(opts = {})
|
|
283
|
+
pwn_ai_run_mission(opts.merge(operation: :steerable))
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Both native tools and regex-ReAct must execute below the same owner.
|
|
287
|
+
# Never retry a failed mission in the terminal/controller process.
|
|
288
|
+
def pwn_ai_run_legacy(opts = {})
|
|
289
|
+
result = pwn_ai_run_mission(opts.merge(operation: :legacy))
|
|
290
|
+
PWN::Env[:ai][result.fetch(:engine).to_sym][:response_history] = result[:response_history]
|
|
291
|
+
rescue StandardError => e
|
|
292
|
+
warn "[pwn-ai] legacy agent mission failed (#{e.class}: #{e.message.to_s.split("\n").first}); no uncontained retry was attempted"
|
|
293
|
+
ensure
|
|
294
|
+
opts[:request]&.replace('nil')
|
|
295
|
+
PWN::Plugins::REPL.ready_tty!
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def pwn_ai_run_mission(opts = {})
|
|
299
|
+
require 'io/console'
|
|
300
|
+
input = opts[:input] || $stdin
|
|
301
|
+
reader, writer = IO.pipe
|
|
302
|
+
run = proc do
|
|
303
|
+
config = %i[pwn_ai_session_id pwn_ai_debug pwn_ai_trace pwn_ai_speak pwn_ai_agent].to_h { |key| [key, opts[:pry]&.config&.public_send(key)] }
|
|
304
|
+
descriptor = { operation: opts.fetch(:operation), config: config, options: opts.except(:pry, :input, :on_tool, :steering, :debug_tee) }
|
|
305
|
+
mission = PWN::Plugins::AISandbox::MissionProcess.new(descriptor: descriptor, fds: { 4 => writer, 0 => input })
|
|
306
|
+
writer.close
|
|
307
|
+
envelope = {}
|
|
308
|
+
reader.each_line do |line|
|
|
309
|
+
frame = JSON.parse(line, symbolize_names: true)
|
|
310
|
+
if frame[:event] == 'tool'
|
|
311
|
+
opts[:on_tool]&.call(*frame[:args])
|
|
312
|
+
else
|
|
313
|
+
envelope = frame
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
raise IOError, envelope[:error] if envelope[:error]
|
|
317
|
+
|
|
318
|
+
envelope[:result]
|
|
319
|
+
ensure
|
|
320
|
+
mission&.close
|
|
321
|
+
end
|
|
322
|
+
input.tty? ? input.cooked(&run) : run.call
|
|
323
|
+
ensure
|
|
324
|
+
[reader, writer].compact.each { |io| io.close unless io.closed? }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def pwn_ai_mission_entry(opts = {})
|
|
328
|
+
pry = Pry.new
|
|
329
|
+
opts.fetch(:config).each { |key, value| pry.config.public_send("#{key}=", value) }
|
|
330
|
+
writer = PWN::AI::Agent::MissionNamespace.io(4)
|
|
331
|
+
options = { trace: pry.config.pwn_ai_trace }.merge(opts.fetch(:options)).merge(
|
|
332
|
+
pry: pry, input: PWN::AI::Agent::MissionNamespace.io(0),
|
|
333
|
+
on_tool: ->(*args) { writer.puts(JSON.generate(event: 'tool', args: args)) }
|
|
334
|
+
)
|
|
335
|
+
begin
|
|
336
|
+
result = opts[:operation] == :legacy ? pwn_ai_run_legacy_child(options) : pwn_ai_run_steerable_child(options)
|
|
337
|
+
writer.puts(JSON.generate(result: result))
|
|
338
|
+
rescue StandardError => e
|
|
339
|
+
writer.puts(JSON.generate(error: "#{e.class}: #{e.message}"))
|
|
340
|
+
ensure
|
|
341
|
+
writer.close
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def pwn_ai_run_legacy_child(opts = {})
|
|
346
|
+
request = opts[:request]
|
|
347
|
+
orig_request = opts[:original_request]
|
|
348
|
+
pi = opts[:pry]
|
|
349
|
+
# Do NOT rebind the 'request' parameter (the string object passed by Pry's after_read hook).
|
|
350
|
+
# We will mutate it to 'nil' at the end of handling so Pry does not eval the natural-language
|
|
351
|
+
# prompt text as Ruby (which was causing noisy exceptions *after* the green AI response print).
|
|
352
|
+
debug = pi.config.pwn_ai_debug
|
|
353
|
+
engine = PWN::Env[:ai][:active].to_s.downcase.to_sym
|
|
354
|
+
response_history = PWN::Env[:ai][engine][:response_history]
|
|
355
|
+
speak_answer = pi.config.pwn_ai_speak
|
|
356
|
+
is_agent = (pi.config.pwn_ai_agent == true)
|
|
357
|
+
|
|
358
|
+
# pwn-ai agent mode: load skills context for autonomous task carrying
|
|
359
|
+
skills_context = ''
|
|
360
|
+
PWN::Skills.each { |n, m| skills_context += "\n--- SKILL #{n} ---\n#{m[:content].to_s[0, 1200]}\n" } if is_agent && PWN.const_defined?(:Skills) && PWN::Skills.is_a?(Hash)
|
|
361
|
+
|
|
362
|
+
memory_context = ''
|
|
363
|
+
memory_context = PWN::Memory.to_context(limit: 25) if is_agent && PWN.const_defined?(:Memory)
|
|
364
|
+
|
|
365
|
+
sess_id = begin
|
|
366
|
+
pi.config.pwn_ai_session_id
|
|
367
|
+
rescue StandardError
|
|
368
|
+
nil
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Pre-process for clear CLI execution intent (e.g. "what does `id` return?")
|
|
372
|
+
# This makes the agent actually *run* commands instead of just explaining them.
|
|
373
|
+
curr_req = orig_request.chomp
|
|
374
|
+
if is_agent && sess_id && PWN.const_defined?(:Sessions)
|
|
375
|
+
begin
|
|
376
|
+
PWN::Sessions.append(session_id: sess_id, role: 'user', content: orig_request)
|
|
377
|
+
rescue StandardError
|
|
378
|
+
nil
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
if is_agent && request =~ /`([^`]+)`/
|
|
382
|
+
potential = ::Regexp.last_match(1).strip
|
|
383
|
+
# Looks like a shell command (not PWN ruby)
|
|
384
|
+
unless potential =~ /^(PWN::|def |class |require |puts |pp )/
|
|
385
|
+
curr_req = "The user wants the *actual raw output* of this command (do not just describe it): `#{potential}`. " \
|
|
386
|
+
'To fulfill the request accurately, you MUST immediately output ONLY a bash code block with the exact command. ' \
|
|
387
|
+
"Example format: ```bash\n#{potential}\n``` . After the host executes it, you will receive the OBSERVATION with the real output."
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Strict system prompt for agent mode (forces tool use over explanation)
|
|
392
|
+
system_role = nil
|
|
393
|
+
if is_agent
|
|
394
|
+
base = PWN::Env[:ai][engine][:system_role_content] || 'You are an ethical hacker.'
|
|
395
|
+
system_role = base + <<~PROMPT
|
|
396
|
+
|
|
397
|
+
You are operating as an autonomous agent inside the PWN REPL driver.
|
|
398
|
+
|
|
399
|
+
PRIMARY RULE FOR CLI AND TOOLS: When the user asks for the output of a command, "what does X return?", "run X", or anything that requires real execution, you MUST use a tool call.#{' '}
|
|
400
|
+
NEVER just explain what a command does or what its output "would be".#{' '}
|
|
401
|
+
To execute anything:
|
|
402
|
+
- Output *exactly and only* a fenced code block.
|
|
403
|
+
- For shell/CLI: ```bash
|
|
404
|
+
<exact command here>
|
|
405
|
+
```
|
|
406
|
+
- For PWN Ruby modules: ```ruby
|
|
407
|
+
PWN::Plugins::NmapIt.port_scan(...)
|
|
408
|
+
```
|
|
409
|
+
The host will execute it (Ruby in full PWN context, bash via shell) and reply with an OBSERVATION containing the real result.#{' '}
|
|
410
|
+
Then continue or give the final answer.
|
|
411
|
+
|
|
412
|
+
Available tools include all PWN::Plugins (NmapIt, TransparentBrowser, etc.), SAST, Reports, and any CLI via bash blocks.
|
|
413
|
+
Skills available this session:#{skills_context}
|
|
414
|
+
#{memory_context}
|
|
415
|
+
|
|
416
|
+
PERSISTENT CAPABILITIES (use via ruby code blocks or direct calls):
|
|
417
|
+
- Memory (cross-session): PWN::Memory.remember(key: :key, value: val, category: :fact|:preference|:lesson)
|
|
418
|
+
PWN::Memory.recall(query: 'foo'), PWN::Memory.forget(key: key)
|
|
419
|
+
- Sessions: current session id = #{sess_id}; PWN::Sessions.append(session_id: '#{sess_id}', role: 'observation', content: obs)
|
|
420
|
+
- Cron: PWN::Cron.create(schedule: '0 * * * *', prompt: 'task here', name: 'foo')
|
|
421
|
+
PWN::Cron.run(id: 'id'); list with PWN::Cron.list
|
|
422
|
+
- Agents/Delegation: PWN::AI::Agent::SAST.analyze(request: ...); PWN::AI::Agent::VulnGen etc.
|
|
423
|
+
For sub-agents use threads or separate eval calls and feed results back as OBS.
|
|
424
|
+
|
|
425
|
+
After receiving an observation, decide the next step or conclude.
|
|
426
|
+
If you output text without a code block, it will be treated as your final answer to the user.
|
|
427
|
+
PROMPT
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
max_turns = is_agent ? 7 : 1
|
|
431
|
+
turn = 0
|
|
432
|
+
last_response = ''
|
|
433
|
+
tool_was_executed_this_turn = false
|
|
434
|
+
|
|
435
|
+
while turn < max_turns
|
|
436
|
+
chat_opts = {
|
|
437
|
+
request: curr_req,
|
|
438
|
+
response_history: response_history,
|
|
439
|
+
speak_answer: speak_answer,
|
|
440
|
+
spinner: false
|
|
441
|
+
}
|
|
442
|
+
chat_opts[:system_role_content] = system_role if system_role
|
|
443
|
+
|
|
444
|
+
case engine
|
|
445
|
+
when :anthropic
|
|
446
|
+
response = PWN::AI::Anthropic.chat(chat_opts)
|
|
447
|
+
when :gemini
|
|
448
|
+
response = PWN::AI::Gemini.chat(chat_opts)
|
|
449
|
+
when :grok
|
|
450
|
+
response = PWN::AI::Grok.chat(chat_opts)
|
|
451
|
+
when :ollama
|
|
452
|
+
response = PWN::AI::Ollama.chat(chat_opts)
|
|
453
|
+
when :openai
|
|
454
|
+
response = PWN::AI::OpenAI.chat(chat_opts)
|
|
455
|
+
when :openwebui
|
|
456
|
+
response = PWN::AI::OpenWebUI.chat(chat_opts)
|
|
457
|
+
else
|
|
458
|
+
raise "ERROR: Unsupported AI Engine: #{engine}"
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
if response.nil?
|
|
462
|
+
last_response = 'Model not currently supported with API key.'
|
|
463
|
+
else
|
|
464
|
+
if response[:choices].last.keys.include?(:text)
|
|
465
|
+
last_response = response[:choices].last[:text].to_s
|
|
466
|
+
else
|
|
467
|
+
last_response = response[:choices].last[:content].to_s
|
|
468
|
+
end
|
|
469
|
+
response_history = {
|
|
470
|
+
id: response[:id],
|
|
471
|
+
object: response[:object],
|
|
472
|
+
model: response[:model],
|
|
473
|
+
usage: response[:usage]
|
|
474
|
+
}
|
|
475
|
+
response_history[:choices] ||= response[:choices]
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
puts "\n\001\e[32m\002#{last_response}\001\e[0m\002\n\n"
|
|
479
|
+
if is_agent && sess_id && PWN.const_defined?(:Sessions)
|
|
480
|
+
begin
|
|
481
|
+
PWN::Sessions.append(session_id: sess_id, role: 'assistant', content: last_response)
|
|
482
|
+
rescue StandardError
|
|
483
|
+
nil
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
if debug
|
|
488
|
+
puts 'DEBUG: response_history => '
|
|
489
|
+
pp response_history
|
|
490
|
+
end
|
|
491
|
+
PWN::Env[:ai][engine][:response_history] = response_history
|
|
492
|
+
|
|
493
|
+
# === Agent tool execution: parse code blocks from *this* response and actually run them ===
|
|
494
|
+
tool_was_executed_this_turn = false
|
|
495
|
+
if is_agent
|
|
496
|
+
# Robust regex: tolerate language specifier, extra whitespace, and text around the block
|
|
497
|
+
last_response.scan(/```(?:\s*(ruby|bash|sh|shell|zsh))?\s*\n?(.*?)\n?```/m).each do |lang, code|
|
|
498
|
+
code = code.strip
|
|
499
|
+
next if code.empty? || tool_was_executed_this_turn
|
|
500
|
+
|
|
501
|
+
lang = (lang || 'bash').downcase
|
|
502
|
+
puts "\001\e[33m\002[ pwn-ai AGENT EXEC #{lang} ]\e[0m\002 #{code[0..90]}..."
|
|
503
|
+
|
|
504
|
+
obs = ''
|
|
505
|
+
begin
|
|
506
|
+
obs = PWN::Plugins::AISandbox.protect_call do
|
|
507
|
+
if lang == 'ruby'
|
|
508
|
+
require 'stringio'
|
|
509
|
+
old_stdout = $stdout
|
|
510
|
+
$stdout = StringIO.new
|
|
511
|
+
res = eval(code, TOPLEVEL_BINDING) # rubocop:disable Security/Eval -- intentional sandboxed legacy tool evaluation
|
|
512
|
+
captured = $stdout.string
|
|
513
|
+
$stdout = old_stdout
|
|
514
|
+
(captured + "\n=> #{res.inspect}").strip
|
|
515
|
+
else
|
|
516
|
+
require 'open3'
|
|
517
|
+
stdout, stderr, status = Open3.capture3(code)
|
|
518
|
+
output = stdout
|
|
519
|
+
output += "\n[stderr]\n#{stderr}" unless stderr.to_s.strip.empty?
|
|
520
|
+
output += "\n[exit: #{status.exitstatus}]" unless status.success?
|
|
521
|
+
output.strip
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
obs = JSON.generate(obs) unless obs.is_a?(String)
|
|
525
|
+
rescue StandardError => e
|
|
526
|
+
obs = "ERROR executing #{lang} block: #{e.class} - #{e.message}"
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
puts "\001\e[36m\002[OBSERVATION from #{lang}]\001\e[0m\002\n#{obs[0..700]}\n"
|
|
530
|
+
if is_agent && sess_id && PWN.const_defined?(:Sessions)
|
|
531
|
+
begin
|
|
532
|
+
PWN::Sessions.append(session_id: sess_id, role: 'observation', content: obs)
|
|
533
|
+
rescue StandardError
|
|
534
|
+
nil
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Feed real result back to the model as the next "user" message in the loop
|
|
539
|
+
curr_req = "OBSERVATION (#{lang} execution result for previous block):\n#{obs}\n\n" \
|
|
540
|
+
"Now continue fulfilling the original user request: #{orig_request}. " \
|
|
541
|
+
'If the task is complete, give the final answer (no more code blocks). Otherwise output the next needed tool block.'
|
|
542
|
+
|
|
543
|
+
tool_was_executed_this_turn = true
|
|
544
|
+
turn += 1
|
|
545
|
+
break # one execution per model turn for controlled pacing
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# If we executed something, loop to let the model react to the OBS
|
|
550
|
+
next if tool_was_executed_this_turn
|
|
551
|
+
|
|
552
|
+
# No tool executed this turn -> this last_response is the final answer
|
|
553
|
+
break
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# If in agent mode and the model never produced an executable block but the query clearly wanted execution,
|
|
557
|
+
# give one last chance with a strong reminder (helps weaker models like some Ollama ones)
|
|
558
|
+
if is_agent && !tool_was_executed_this_turn && orig_request =~ /`[^`]+`/ && turn < max_turns
|
|
559
|
+
reminder = 'The user explicitly asked about the output of a command in backticks. ' \
|
|
560
|
+
'Do not describe the command. Output *only* the corresponding ```bash block now so the host can run it and give you the real result.'
|
|
561
|
+
curr_req = "#{reminder}\nOriginal: #{orig_request}"
|
|
562
|
+
# One final direct call (no full re-loop to avoid complexity)
|
|
563
|
+
# (The main loop already handled most cases; this is a safety net)
|
|
564
|
+
end
|
|
565
|
+
{ engine: engine, response_history: response_history }
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def pwn_ai_run_steerable_child(opts = {})
|
|
283
569
|
previous = Thread.current[:pwn_steering_input]
|
|
284
570
|
workspace = File.realpath(Dir.pwd)
|
|
285
571
|
lock_path = File.join(Dir.tmpdir, "pwn-solve-#{Digest::SHA256.hexdigest(workspace)}.lock")
|
|
@@ -580,6 +866,16 @@ module PWN
|
|
|
580
866
|
end
|
|
581
867
|
when '/cron'
|
|
582
868
|
pwn_ai_run_cron(args: args)
|
|
869
|
+
when '/maintenance'
|
|
870
|
+
require File.expand_path('../../../../etc/default_cron/session_cleanup', __dir__)
|
|
871
|
+
raise ArgumentError, 'usage: /maintenance [status|run|dry-run]' unless args.empty? || (args.length == 1 && %w[status run dry-run].include?(args.first))
|
|
872
|
+
|
|
873
|
+
result = if args.empty? || args.first == 'status'
|
|
874
|
+
PWN::SessionCleanup.status
|
|
875
|
+
else
|
|
876
|
+
PWN::SessionCleanup.maintain(dry_run: args.first == 'dry-run', current_session_id: pi&.config&.pwn_ai_session_id)
|
|
877
|
+
end
|
|
878
|
+
pwn_ai_print_result(value: result)
|
|
583
879
|
when '/sessions'
|
|
584
880
|
identifier = request.sub(%r{\A/sessions[[:space:]]+resume(?:[[:space:]]+|$)}, '') if args.first == 'resume'
|
|
585
881
|
pwn_ai_run_sessions(args: args, identifier: identifier, pry: pi)
|
|
@@ -851,24 +1147,11 @@ module PWN
|
|
|
851
1147
|
env_path = File.join(Dir.home, '.pwn', 'pwn.yaml') if env_path.empty?
|
|
852
1148
|
dec_path = dec_path.to_s.strip
|
|
853
1149
|
dec_path = "#{env_path}.decryptor" if dec_path.empty?
|
|
854
|
-
return false unless File.exist?(env_path)
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
return false if key.to_s.strip.empty? || iv.to_s.strip.empty?
|
|
860
|
-
|
|
861
|
-
# Never decrypt the live vault in place: failed encryption must leave
|
|
862
|
-
# the previous ciphertext intact. Rename only the encrypted merge.
|
|
863
|
-
require 'tempfile'
|
|
864
|
-
Tempfile.create(['.pwn-ai-selection', '.yaml'], File.dirname(env_path)) do |file|
|
|
865
|
-
file.binmode
|
|
866
|
-
file.write(File.binread(env_path))
|
|
867
|
-
file.flush
|
|
868
|
-
path = file.path
|
|
869
|
-
PWN::Plugins::Vault.decrypt(file: path, key: key, iv: iv)
|
|
870
|
-
cfg = YAML.load_file(path, symbolize_names: true)
|
|
871
|
-
cfg = {} unless cfg.is_a?(Hash)
|
|
1150
|
+
return false unless File.exist?(env_path)
|
|
1151
|
+
|
|
1152
|
+
# Shared update preserves hardware or legacy format and publishes only
|
|
1153
|
+
# ciphertext after a locked re-read and final conflict comparison.
|
|
1154
|
+
PWN::Plugins::Vault.update(file: env_path, decryptor_file: dec_path) do |cfg|
|
|
872
1155
|
cfg[:ai] = {} unless cfg[:ai].is_a?(Hash)
|
|
873
1156
|
cfg[:ai][:active] = engine unless opts.key?(:system_role_content)
|
|
874
1157
|
unless model.to_s.strip.empty?
|
|
@@ -884,15 +1167,10 @@ module PWN
|
|
|
884
1167
|
cfg[:ai][engine.to_sym] ||= {}
|
|
885
1168
|
cfg[:ai][engine.to_sym][:system_role_content] = opts[:system_role_content]
|
|
886
1169
|
end
|
|
887
|
-
yaml_env = YAML.dump(cfg).gsub(/^(\s*):/, '\1')
|
|
888
|
-
File.write(path, yaml_env)
|
|
889
|
-
File.chmod(0o600, path)
|
|
890
|
-
PWN::Plugins::Vault.encrypt(file: path, key: key, iv: iv)
|
|
891
|
-
File.rename(path, env_path)
|
|
892
1170
|
end
|
|
893
1171
|
true
|
|
894
1172
|
rescue StandardError => e
|
|
895
|
-
warn "[pwn-ai] settings persist failed: #{e.class}
|
|
1173
|
+
warn "[pwn-ai] settings persist failed: #{e.class}; check vault access and retry."
|
|
896
1174
|
false
|
|
897
1175
|
end
|
|
898
1176
|
|