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
|
@@ -28,8 +28,8 @@ module PWN
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
# Private, in-memory configuration transaction. No plaintext filesystem stage.
|
|
31
|
-
#
|
|
32
|
-
class VaultEdit
|
|
31
|
+
# Uses the shared vault codec for legacy sidecars and YubiKey envelopes.
|
|
32
|
+
class VaultEdit
|
|
33
33
|
class Error < StandardError; end
|
|
34
34
|
|
|
35
35
|
attr_reader :text
|
|
@@ -82,15 +82,17 @@ module PWN
|
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def serialize(value)
|
|
85
|
-
YAML.dump(value)
|
|
85
|
+
YAML.dump(value)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def credentials
|
|
89
|
+
return PWN::Plugins::Vault.access_options(file: @path) if PWN::Plugins::Vault.hardware_encrypted?(file: @path)
|
|
90
|
+
|
|
89
91
|
safe_file(@decryptor_path)
|
|
90
92
|
result = parse(File.read(@decryptor_path))
|
|
91
93
|
raise Error, 'Decryptor requires the existing key and IV; restore the original sidecar.' unless result.is_a?(Hash) && result[:key].is_a?(String) && result[:iv].is_a?(String)
|
|
92
94
|
|
|
93
|
-
result.slice(:key, :iv)
|
|
95
|
+
PWN::Plugins::Vault.access_options(result.slice(:key, :iv).merge(file: @path, decryptor_file: @decryptor_path))
|
|
94
96
|
end
|
|
95
97
|
|
|
96
98
|
def safe_file(path)
|
|
@@ -154,12 +156,7 @@ module PWN
|
|
|
154
156
|
end
|
|
155
157
|
|
|
156
158
|
def encrypt(text)
|
|
157
|
-
|
|
158
|
-
cipher = OpenSSL::Cipher.new('aes-256-cbc')
|
|
159
|
-
cipher.encrypt
|
|
160
|
-
cipher.key = Base64.strict_decode64(secrets.fetch(:key))
|
|
161
|
-
cipher.iv = Base64.strict_decode64(secrets.fetch(:iv))
|
|
162
|
-
"#{Base64.strict_encode64(cipher.update(text) + cipher.final)}\n"
|
|
159
|
+
PWN::Plugins::Vault.encrypt_data(credentials.merge(data: text))
|
|
163
160
|
end
|
|
164
161
|
|
|
165
162
|
def atomic_write(encrypted, previous)
|
|
@@ -277,6 +274,10 @@ module PWN
|
|
|
277
274
|
class Control < PWN::AI::Agent::Loop::Steering
|
|
278
275
|
class Stopped < Exception; end # rubocop:disable Lint/InheritException
|
|
279
276
|
|
|
277
|
+
def stopping?
|
|
278
|
+
@stopping == true
|
|
279
|
+
end
|
|
280
|
+
|
|
280
281
|
def stop
|
|
281
282
|
@stopping = true
|
|
282
283
|
request_model_cancel
|
|
@@ -460,6 +461,40 @@ module PWN
|
|
|
460
461
|
end
|
|
461
462
|
end
|
|
462
463
|
|
|
464
|
+
# JSON-only, generation-tagged child events; never deserialize Ruby objects.
|
|
465
|
+
class MissionEvents
|
|
466
|
+
def initialize(io, generation)
|
|
467
|
+
@io = io
|
|
468
|
+
@generation = generation
|
|
469
|
+
@mutex = Mutex.new
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def <<(event)
|
|
473
|
+
@mutex.synchronize { @io.write("#{JSON.generate([@generation, *event])}\n") }
|
|
474
|
+
self
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Parent-side steering endpoint; cancellation belongs to the process owner.
|
|
479
|
+
class MissionControl
|
|
480
|
+
def initialize(io)
|
|
481
|
+
@io = io
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def submit(text)
|
|
485
|
+
@io.puts(JSON.generate(text.to_s))
|
|
486
|
+
@io.flush
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def phase
|
|
490
|
+
'mission process'
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def stop
|
|
494
|
+
:stopping
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
463
498
|
# Own the terminal, event pump and the lifetime of exactly one request.
|
|
464
499
|
class Console # rubocop:disable Metrics/ClassLength -- cohesive single-owner terminal state machine
|
|
465
500
|
COLORS = { operator: :operator, task: :task, thinking: :notice, tool: :tool, result: :result, assistant: :assistant, notice: :notice, warning: :warning }.freeze
|
|
@@ -612,8 +647,27 @@ module PWN
|
|
|
612
647
|
|
|
613
648
|
def drain
|
|
614
649
|
200.times do
|
|
615
|
-
type, text = @events.pop(true)
|
|
650
|
+
type, text, generation = @events.pop(true)
|
|
651
|
+
next if generation && generation != @mission_generation && generation != @swarm_generation
|
|
652
|
+
|
|
616
653
|
case type
|
|
654
|
+
when :command_state
|
|
655
|
+
if text[:ai].is_a?(Hash)
|
|
656
|
+
text[:ai][:active] = text[:ai][:active].to_sym if PWN::Env.dig(:ai, :active).is_a?(Symbol) && text[:ai][:active].is_a?(String)
|
|
657
|
+
PWN::Env[:ai] = text[:ai]
|
|
658
|
+
end
|
|
659
|
+
text.fetch(:config, {}).each do |key, value|
|
|
660
|
+
@pry.config.public_send("#{key}=", value) if %i[pwn_ai_session_id pwn_ai_debug pwn_ai_trace].include?(key)
|
|
661
|
+
end
|
|
662
|
+
when :usage
|
|
663
|
+
@usage.record(text)
|
|
664
|
+
when :mission_done
|
|
665
|
+
@worker.join
|
|
666
|
+
finish_request_metrics
|
|
667
|
+
@running = false
|
|
668
|
+
@control = nil
|
|
669
|
+
@mission = nil
|
|
670
|
+
@mission_commands&.close unless @mission_commands&.closed?
|
|
617
671
|
when :operator_loaded
|
|
618
672
|
generation, data = text
|
|
619
673
|
if @operator_view && @operator_view[:generation] == generation
|
|
@@ -691,97 +745,230 @@ module PWN
|
|
|
691
745
|
PWN::Plugins::Log.mirror_tui!(msg: "#{type.to_s.upcase}\n#{clean}") if @pry.config.pwn_ai_debug && type != :notice
|
|
692
746
|
end
|
|
693
747
|
|
|
694
|
-
def
|
|
748
|
+
private_class_method def self.mission_entry(opts = {})
|
|
749
|
+
data = opts[:descriptor]
|
|
750
|
+
console = allocate
|
|
751
|
+
pry = Pry.new
|
|
752
|
+
data.fetch(:config).each { |key, value| pry.config.public_send("#{key}=", value) }
|
|
753
|
+
console.instance_variable_set(:@pry, pry)
|
|
754
|
+
console.instance_variable_set(:@input, PWN::AI::Agent::MissionNamespace.io(5))
|
|
755
|
+
writer = PWN::AI::Agent::MissionNamespace.io(4)
|
|
756
|
+
if data[:operation] == :console
|
|
757
|
+
console.send(:mission_request_child, data, writer, PWN::AI::Agent::MissionNamespace.io(6))
|
|
758
|
+
else
|
|
759
|
+
console.send(:mission_command_child, data[:request], data[:generation], data[:baseline_ai], writer, data[:cleanup])
|
|
760
|
+
end
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def mission_config
|
|
764
|
+
%i[pwn_ai_session_id pwn_ai_debug pwn_ai_trace pwn_ai_speak pwn_ai_agent].to_h { |key| [key, @pry.config.public_send(key)] }
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def mission_request_child(data, writer, commands)
|
|
768
|
+
request, generation, session_id, title_config = data.values_at(:request, :generation, :session_id, :title_config)
|
|
769
|
+
events = MissionEvents.new(writer, generation)
|
|
770
|
+
output = EventIO.new(events)
|
|
771
|
+
$stdout = $stderr = output
|
|
772
|
+
$stdin = @input
|
|
773
|
+
@pry.config.output = output
|
|
774
|
+
control = Control.new(input: @input, output: output)
|
|
775
|
+
Thread.current[:pwn_steering_input] = control
|
|
776
|
+
Thread.current[:pwn_usage_observer] = ->(usage) { events << [:usage, usage] }
|
|
777
|
+
Thread.new do
|
|
778
|
+
commands.each_line { |line| control.submit(JSON.parse(line)) }
|
|
779
|
+
end
|
|
780
|
+
begin
|
|
781
|
+
workspace = File.realpath(Dir.pwd)
|
|
782
|
+
lock_path = File.join(Dir.tmpdir, "pwn-solve-#{Digest::SHA256.hexdigest(workspace)}.lock")
|
|
783
|
+
File.open(lock_path, File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lease|
|
|
784
|
+
raise IOError, 'Workspace busy: another mission owns this directory.' unless lease.flock(File::LOCK_EX | File::LOCK_NB)
|
|
785
|
+
|
|
786
|
+
events << [:session_title, PWN::Sessions.provisional_title(session_id: session_id, request: request)]
|
|
787
|
+
on_tool = lambda do |name, args, result|
|
|
788
|
+
if %w[task thinking].include?(name.to_s)
|
|
789
|
+
events << [name.to_sym, args.to_s]
|
|
790
|
+
else
|
|
791
|
+
events << [:tool, "#{name}\n#{args.is_a?(String) ? args : args.inspect}"]
|
|
792
|
+
events << [:result, result.to_s]
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
final = PWN::AI::Agent::Loop.run(request: request, session_id: session_id,
|
|
796
|
+
enabled_toolsets: PWN::Env.dig(:ai, :agent, :toolsets),
|
|
797
|
+
on_tool: on_tool, steering: control, debug: @pry.config.pwn_ai_debug,
|
|
798
|
+
trace: @pry.config.pwn_ai_trace, debug_tee: output)
|
|
799
|
+
promoted = Thread.current[:pwn_thinking_promoted] && final.to_s.strip == Thread.current[:pwn_last_thinking].to_s.strip
|
|
800
|
+
events << [:assistant, final.to_s] unless promoted
|
|
801
|
+
events << [:session_title, PWN::Sessions.refresh_title(session_id: session_id, ai_config: title_config, on_error: ->(kind) { events << [:warning, "Session title refresh unavailable (#{kind}); previous title retained."] })]
|
|
802
|
+
end
|
|
803
|
+
rescue StandardError, Interrupt, Control::Stopped => e
|
|
804
|
+
events << [:warning, "#{e.class}: #{e.message}"]
|
|
805
|
+
ensure
|
|
806
|
+
output.flush
|
|
807
|
+
writer.close
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def mission_command_child(request, generation, baseline_ai, writer, cleanup)
|
|
812
|
+
events = MissionEvents.new(writer, generation)
|
|
813
|
+
output = EventIO.new(events)
|
|
814
|
+
$stdout = $stderr = output
|
|
815
|
+
$stdin = @input
|
|
816
|
+
@pry.config.output = output
|
|
817
|
+
Thread.current[:pwn_steering_input] = Control.new(input: @input, output: output)
|
|
818
|
+
@events = events
|
|
819
|
+
@output = output
|
|
820
|
+
begin
|
|
821
|
+
if cleanup
|
|
822
|
+
result = PWN::SessionCleanup.run(dry_run: false, current_session_id: @pry.config.pwn_ai_session_id, approved_candidates: cleanup)
|
|
823
|
+
events << [:command, REPL.pwn_ai_result_text(value: result)]
|
|
824
|
+
elsif request.match?(%r{\A/model(?:\s|$)})
|
|
825
|
+
REPL.pwn_ai_run_model(args: request.split.drop(1), prompt: lambda { |selection|
|
|
826
|
+
events << [:model_prompt, selection]
|
|
827
|
+
:deferred
|
|
828
|
+
})
|
|
829
|
+
else
|
|
830
|
+
REPL.pwn_ai_dispatch_slash!(request: request, pry: @pry, raise_errors: true)
|
|
831
|
+
end
|
|
832
|
+
config = %i[pwn_ai_session_id pwn_ai_debug pwn_ai_trace].to_h { |key| [key, @pry.config.public_send(key)] }
|
|
833
|
+
ai = PWN::Env[:ai] unless JSON.generate(PWN::Env[:ai]) == baseline_ai
|
|
834
|
+
events << [:command_state, { ai: ai, config: config }]
|
|
835
|
+
rescue StandardError, Interrupt, Control::Stopped => e
|
|
836
|
+
events << [:warning, "#{e.class}: #{e.message}"]
|
|
837
|
+
ensure
|
|
838
|
+
output.flush
|
|
839
|
+
writer.close
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def start_mission_request(request)
|
|
844
|
+
return add(:warning, 'A mission is still running; cancel and reap it first.') if busy?
|
|
845
|
+
|
|
846
|
+
@mission_generation = SecureRandom.uuid
|
|
847
|
+
generation = @mission_generation
|
|
695
848
|
session_id = @pry.config.pwn_ai_session_id
|
|
696
849
|
title_config = Marshal.load(Marshal.dump(PWN::Env[:ai]))
|
|
697
|
-
@
|
|
698
|
-
@thread_variables = Thread.current.thread_variables.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, Thread.current.thread_variable_get(key)] }
|
|
699
|
-
# A fresh forwarding pipe prevents unread input crossing requests.
|
|
700
|
-
@tool_input.close unless @tool_input.closed?
|
|
850
|
+
@tool_input&.close unless @tool_input&.closed?
|
|
701
851
|
forwarded, @tool_input = IO.pipe
|
|
702
852
|
@input.reopen(forwarded)
|
|
703
853
|
forwarded.close
|
|
704
|
-
@cancelling = false
|
|
705
|
-
@command = command
|
|
854
|
+
@cancelling = @command = false
|
|
706
855
|
begin_request_metrics
|
|
707
856
|
add(:operator, request)
|
|
857
|
+
reader, writer = IO.pipe
|
|
858
|
+
commands, @mission_commands = IO.pipe
|
|
859
|
+
@mission = PWN::Plugins::AISandbox::MissionProcess.new(
|
|
860
|
+
descriptor: { operation: :console, request: request, generation: generation, session_id: session_id, title_config: title_config, config: mission_config },
|
|
861
|
+
fds: { 4 => writer, 5 => @input, 6 => commands }
|
|
862
|
+
)
|
|
863
|
+
writer.close
|
|
864
|
+
commands.close
|
|
865
|
+
@control = MissionControl.new(@mission_commands)
|
|
708
866
|
@running = true
|
|
709
|
-
|
|
710
|
-
gate = Queue.new
|
|
867
|
+
mission = @mission
|
|
711
868
|
@worker = Thread.new do
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
ready << control
|
|
718
|
-
gate.pop
|
|
719
|
-
on_tool = lambda do |name, args, result|
|
|
720
|
-
if name.to_s == 'task'
|
|
721
|
-
@events << [:task, args.to_s]
|
|
722
|
-
elsif name.to_s == 'thinking'
|
|
723
|
-
@events << [:thinking, args.to_s]
|
|
724
|
-
else
|
|
725
|
-
@events << [:tool, "#{name}\n#{args.is_a?(String) ? args : args.inspect}"]
|
|
726
|
-
@events << [:result, result.to_s]
|
|
727
|
-
end
|
|
869
|
+
reader.each_line do |line|
|
|
870
|
+
tag, type, text = JSON.parse(line, symbolize_names: true)
|
|
871
|
+
next unless tag == generation && %w[task thinking tool result assistant notice warning session_title usage].include?(type)
|
|
872
|
+
|
|
873
|
+
@events << [type.to_sym, text, tag]
|
|
728
874
|
end
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
875
|
+
rescue StandardError => e
|
|
876
|
+
@events << [:warning, "Mission IPC: #{e.message}", generation]
|
|
877
|
+
ensure
|
|
878
|
+
mission.close
|
|
879
|
+
reader.close
|
|
880
|
+
@events << [:mission_done, nil, generation]
|
|
881
|
+
end
|
|
882
|
+
@worker.report_on_exception = false
|
|
883
|
+
rescue StandardError => e
|
|
884
|
+
[reader, writer, commands, @mission_commands].compact.each { |io| io.close unless io.closed? }
|
|
885
|
+
@mission&.close
|
|
886
|
+
@mission = nil
|
|
887
|
+
@running = false
|
|
888
|
+
add(:warning, "Mission not started: #{e.message}")
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def cancel_mission
|
|
892
|
+
# Invalidate queued events first. Ready is set only after the reader
|
|
893
|
+
# reaps namespace PID 1, including all provider and tool descendants.
|
|
894
|
+
@cancelling = true
|
|
895
|
+
@mission_generation = nil
|
|
896
|
+
@mission.close
|
|
897
|
+
@worker&.join
|
|
898
|
+
@mission_commands&.close unless @mission_commands&.closed?
|
|
899
|
+
@tool_input&.close unless @tool_input&.closed?
|
|
900
|
+
@mission = @control = nil
|
|
901
|
+
@running = false
|
|
902
|
+
finish_request_metrics
|
|
903
|
+
add(:warning, 'Mission cancelled; all owned processes reaped. Completed work is not undone.')
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
def start_request(request, command: false, cleanup: nil)
|
|
907
|
+
return start_mission_request(request) unless command
|
|
908
|
+
return add(:warning, 'A mission is still running; cancel and reap it first.') if busy?
|
|
909
|
+
|
|
910
|
+
@mission_generation = SecureRandom.uuid
|
|
911
|
+
generation = @mission_generation
|
|
912
|
+
@tool_input&.close unless @tool_input&.closed?
|
|
913
|
+
forwarded, @tool_input = IO.pipe
|
|
914
|
+
@input.reopen(forwarded)
|
|
915
|
+
forwarded.close
|
|
916
|
+
@cancelling = false
|
|
917
|
+
@command = true
|
|
918
|
+
baseline_ai = JSON.generate(PWN::Env[:ai])
|
|
919
|
+
begin_request_metrics
|
|
920
|
+
add(:operator, request)
|
|
921
|
+
reader, writer = IO.pipe
|
|
922
|
+
@mission = PWN::Plugins::AISandbox::MissionProcess.new(
|
|
923
|
+
descriptor: { operation: :slash, request: request, generation: generation, baseline_ai: baseline_ai, config: mission_config, cleanup: cleanup },
|
|
924
|
+
fds: { 4 => writer, 5 => @input }
|
|
925
|
+
)
|
|
926
|
+
writer.close
|
|
927
|
+
@running = true
|
|
928
|
+
mission = @mission
|
|
929
|
+
@worker = Thread.new do
|
|
930
|
+
reader.each_line do |line|
|
|
931
|
+
tag, type, text = JSON.parse(line, symbolize_names: true)
|
|
932
|
+
next unless tag == generation && %w[notice warning command command_notice model_prompt command_state].include?(type)
|
|
933
|
+
|
|
934
|
+
@events << [type.to_sym, text, tag]
|
|
762
935
|
end
|
|
763
|
-
rescue
|
|
764
|
-
@events << [:warning, e.message]
|
|
765
|
-
rescue StandardError, Interrupt => e
|
|
766
|
-
Thread.current[:pwn_log_progress] = false
|
|
767
|
-
PWN::Plugins::Log.note_exception!(error: e, where: 'curses agent', which_self: AIConsole)
|
|
768
|
-
@events << [:warning, "#{e.class}: #{e.message}"]
|
|
936
|
+
rescue StandardError => e
|
|
937
|
+
@events << [:warning, "Command IPC: #{e.message}", generation]
|
|
769
938
|
ensure
|
|
770
|
-
|
|
771
|
-
|
|
939
|
+
mission.close
|
|
940
|
+
reader.close
|
|
941
|
+
@events << [:mission_done, nil, generation]
|
|
772
942
|
end
|
|
773
943
|
@worker.report_on_exception = false
|
|
774
|
-
|
|
775
|
-
|
|
944
|
+
rescue StandardError => e
|
|
945
|
+
[reader, writer].compact.each { |io| io.close unless io.closed? }
|
|
946
|
+
@mission&.close
|
|
947
|
+
@mission = nil
|
|
948
|
+
@running = false
|
|
949
|
+
add(:warning, "Command not started: #{e.message}")
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
def swarm_event_callback
|
|
953
|
+
@swarm_generation ||= SecureRandom.uuid
|
|
954
|
+
generation = @swarm_generation
|
|
955
|
+
->(type, text) { @events << [type, text, generation] }
|
|
776
956
|
end
|
|
777
957
|
|
|
778
958
|
def cancel
|
|
959
|
+
@swarm_generation = SecureRandom.uuid
|
|
960
|
+
if @mission
|
|
961
|
+
cancel_mission
|
|
962
|
+
@swarm&.cancel_all
|
|
963
|
+
return
|
|
964
|
+
end
|
|
965
|
+
|
|
779
966
|
if busy?
|
|
780
967
|
first = !@cancelling
|
|
781
968
|
@cancelling = true
|
|
782
969
|
@control&.stop
|
|
783
970
|
@swarm&.cancel_all
|
|
784
|
-
@tool_input
|
|
971
|
+
@tool_input&.close unless @tool_input&.closed?
|
|
785
972
|
add(:warning, 'Cancellation requested. Waiting for a safe boundary; work already started is not undone.') if first
|
|
786
973
|
else
|
|
787
974
|
@editor.replace('')
|
|
@@ -789,11 +976,12 @@ module PWN
|
|
|
789
976
|
end
|
|
790
977
|
|
|
791
978
|
def stop_worker
|
|
979
|
+
cancel_mission if @mission
|
|
792
980
|
return unless @worker&.alive? || @swarm&.busy?
|
|
793
981
|
|
|
794
982
|
@control&.stop
|
|
795
983
|
@swarm&.cancel_all
|
|
796
|
-
@tool_input
|
|
984
|
+
@tool_input&.close unless @tool_input&.closed?
|
|
797
985
|
@worker&.join
|
|
798
986
|
@swarm&.close
|
|
799
987
|
ensure
|
|
@@ -848,7 +1036,7 @@ module PWN
|
|
|
848
1036
|
open_vault_edit(line)
|
|
849
1037
|
elsif line.strip == '/system-role'
|
|
850
1038
|
open_system_role
|
|
851
|
-
elsif line.match?(%r{\A/(?:mcp(?:\s|$)|cron\s+run(?:\s|$)|model(?:\s|$)|sessions\s+cleanup(?:\s|$))})
|
|
1039
|
+
elsif line.match?(%r{\A/(?:maintenance(?:\s|$)|mcp(?:\s|$)|cron\s+run(?:\s|$)|model(?:\s|$)|sessions\s+cleanup(?:\s|$))})
|
|
852
1040
|
start_request(line, command: true)
|
|
853
1041
|
elsif line.start_with?('/') || line.start_with?('ai.profile ', 'ai.memory ')
|
|
854
1042
|
if line.match?(%r{\A/sessions[[:space:]]+resume(?:[[:space:]]|$)})
|
|
@@ -910,7 +1098,7 @@ module PWN
|
|
|
910
1098
|
|
|
911
1099
|
return add(:warning, 'Workspace busy: wait or cancel the ordinary request before launching a swarm writer.') if @running && line.match?(%r{\A/swarm\s+(?:mission|dm|broadcast|debate)\b})
|
|
912
1100
|
|
|
913
|
-
result = @swarm.execute(mission_options.merge(line: line, on_event:
|
|
1101
|
+
result = @swarm.execute(mission_options.merge(line: line, on_event: swarm_event_callback, usage_observer: @usage.method(:record)))
|
|
914
1102
|
text = result[:job_id] ? "Swarm job #{result[:job_id]} queued (#{result[:state]})." : (result[:usage] || REPL.pwn_ai_result_text(value: result))
|
|
915
1103
|
add(:command, text)
|
|
916
1104
|
end
|
|
@@ -2406,13 +2594,9 @@ module PWN
|
|
|
2406
2594
|
|
|
2407
2595
|
view = @operator_view
|
|
2408
2596
|
approved = view[:plan][:records].select { |row| view[:plan][:candidate_ids].include?(row[:id]) }.to_h { |row| [row[:id], { files: row[:files], dependency_records: row[:dependency_records] }] }
|
|
2409
|
-
session_id = @pry.config.pwn_ai_session_id
|
|
2410
2597
|
view.delete(:confirm)
|
|
2411
2598
|
@operator_view = nil
|
|
2412
|
-
start_request('/sessions cleanup (approved preview)', command: true)
|
|
2413
|
-
result = PWN::SessionCleanup.run(dry_run: false, current_session_id: session_id, approved_candidates: approved)
|
|
2414
|
-
@events << [:command, REPL.pwn_ai_result_text(value: result)]
|
|
2415
|
-
end
|
|
2599
|
+
start_request('/sessions cleanup (approved preview)', command: true, cleanup: approved)
|
|
2416
2600
|
end
|
|
2417
2601
|
|
|
2418
2602
|
def operator_viewport(page)
|
|
@@ -2577,7 +2761,7 @@ module PWN
|
|
|
2577
2761
|
@workspace[:notice] = 'Workspace busy: wait or cancel the ordinary request first.'
|
|
2578
2762
|
return
|
|
2579
2763
|
end
|
|
2580
|
-
options = mission_options.merge(line: Shellwords.join(['/swarm'] + args), on_event:
|
|
2764
|
+
options = mission_options.merge(line: Shellwords.join(['/swarm'] + args), on_event: swarm_event_callback, usage_observer: @usage.method(:record))
|
|
2581
2765
|
options[:solve_request] = args.last if args.first == 'mission'
|
|
2582
2766
|
result = @swarm.execute(options)
|
|
2583
2767
|
@workspace[:notice] = result[:error] || "#{args.first}: accepted#{" · job #{result[:job_id]}" if result[:job_id]}"
|